青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-
友链 搜索文章 >>

青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-

【C语言学习笔记】八、指针


阅读数: 0次    2025-12-24
字数:9.2k字 | 预计阅读时长:38分钟

1. 地址

计算机中的地址(address)一般指代内存中的某一个存储单元的地址,可以简单理解为某一个内存存储单元的编号。

地址是内存中存储数据的位置的唯一标识。对于储存在内存中的变量,只要得知这个变量的地址,即可非常快速地拿到它的值,并对其进行修改等操作。

指向内存中的某一块地址的一个特殊的变量叫做指针。

2. 取地址的含义

取地址符号为&,可以使用&拿到一个变量的地址。

在printf()函数中,使用%p可以打印一个地址类型的数据。

为了规范,发生强制类型转换的地方一般要写上转换后的数据类型,(void*)代表将变量强制转换为指针类型的数据。

例如一个快递员找目标住户的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

// C语言案例
int main(void) {
uint32_t building_numbers[5] = { 101, 102, 103, 104, 105 }; // 住户门牌号数组
uint32_t target_number = 103; // 目标住户门牌号

bool found = false; // 标记是否找到目标住户

printf("快递员开始寻找门牌号为 %" PRIu32 " 的住户。\n", target_number);

// 打印每个住户的地址
for (uint32_t i = 0; i < 5; i++)
{
// 使用 %p 格式说明符打印地址
// 使用 & 运算符获取变量的地址
// (void*) 强制转换为 void* 类型以匹配 %p 的要求
printf("住户 %" PRIu32 " 的地址为:%p\n", building_numbers[i], (void*)&building_numbers[i]);
}

// 快递员寻找目标住户
for (uint32_t i = 0; i < 5; i++) {
printf("快递员检查住户门牌号:%" PRIu32 "\n", building_numbers[i]);
if (building_numbers[i] == target_number) {
printf("快递员找到了目标住户,门牌号为:%" PRIu32 ",地址为:%p\n", building_numbers[i], (void*)&building_numbers[i]);
found = true;
break;
}
}
if (!found) {
printf("快递员未能找到门牌号为 %" PRIu32 " 的住户。\n", target_number);
}
else
{
printf("快递员成功完成投递任务!\n");
}
return 0;
}

通过打印信息可以很明显的看出,一个数组中的所有元素在内存中都是紧挨着的,不会中断。

由于数组中元素的类型为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
2
int number = 123;
int* ptr_number = &number;

可以有以下特性:

  • &是取地址运算符:&number得到变量number的地址,类型为int*;
  • ptr_number的类型为int*,保存了number的地址;
  • *ptr_number表示“位于ptr_number所指地址的那个int对象”,类型为int。因此*ptr_number和number指向同一块内存——读取时值相同,同时写入*ptr_number会改变number的值。

也就是下面三个printf语句的输出值相同:

1
2
3
printf("number = %d\n", number);
printf("number = %d\n", *ptr_number);
printf("number = %d\n", *(&number));

可以通过指针修改变量的值,例如:

1
2
3
4
5
int number = 123;
int* ptr_number = &number;
printf("修改前,number = %d\n", number);
*ptr_number = 456; // 通过指针修改 number 的值
printf("修改后,number = %d\n", number);

5. 指针星号的企业风格规范以及容易引发的问题

定义指针时,有两种常见的写法:

int* p:微软写法,强调p这个变量是一个int*(整型指针)类型的变量

int *p:强调这个变量实际上是一个指针

这两种定义方式都可以,但较为规范且易于理解的写法应该第一种int* p。

值得注意的是,当想要在一个语句中同时定义两个指针时:

1
2
3
4
5
6
7
8
// 错误写法:
int* p, q; // 这里q不是指针,而是一个整数
// 相当于:
int* p;
int q;

// 正确写法:
int* p, * q; // 这里p和q都是指针

因此,应该尽可能避免在一个语句中同时定义多个变量,防止出现类似的隐性问题。

6. 指针的意义与作用究竟是什么:外部服务操作

指针存在的意义在于:希望外部能够提供更好的服务给我,而我只需要给他地址,他就可以帮我解决一切的困扰,减少我亲自操作的麻烦。

简单来说,Windows系统中的快捷方式就是一种指针的应用,快捷方式是一个指向真实文件或文件夹路径的指针,指针不需要管点击后文件如何打开,以及程序是如何运行的,它只需要指向一个地址即可。

7. 野指针初步介绍

野指针:指向了一个无效的内存地址或者是已经释放的内存地址的指针。

非常危险!

野指针会访问一个不可描述的内存空间,会导致不可预测的行为。

访问或操作野指针可能会导致拿到奇怪的数据或者对错误地址的数据执行了一些操作,会导致无法预测的结果。

程序中必须对野指针做判断和处理。

实际上,比较高级的编译器会自动识别野指针并报错。

8. 空指针初步介绍

空指针:通常指没有指向任何有效内存地址的指针。

C语言或其他高级语言中常见的“空指针异常”,就是指此处的空指针处理出错。

C语言中允许先创建空指针后赋地址,例如:

1
char* ptr = NULL;

9. 空指针的初始化

指针初始化时可以设置它为空指针,即赋值为NULL,后续可以将一个地址写入空指针。

在Visual Studio中,打印空指针的地址会得到全0的地址,尝试对空指针所指向的数据进行解引用处理时,会导致运行时错误。

因此,程序中需要对可能出现空指针的情况做判断处理,防止出现以外解引用空指针导致的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// C语言案例
int main(void) {
// 空指针
uint32_t* ptr_number = NULL;

// 指向一个有效地址
uint32_t number = 100;
ptr_number = &number;

// 判断空指针
if (ptr_number == NULL) {
printf("指针为空\n");
}
else {
// 这里展示了两种打印指针地址的方式
printf("指针不为空,地址为: %" PRIXPTR "\n", (uintptr_t)ptr_number); // PRIXPTR 用于打印指针地址,以十六进制大写形式显示,类型转换为 uintptr_t,确保兼容性
printf("指针不为空,地址为: %p\n", (void*)ptr_number); // %p 用于打印指针地址,以平台相关的格式显示,类型转换为 void*,确保兼容性
printf("指针指向的值为: %" PRIu32 "\n", *ptr_number);
}
return 0;
}

10. 从代码上尝试认识野指针

野指针不像空指针可以直接定义,空指针只需要做好判断和确保赋地址即可,野指针是一个代码中可能出现的问题,需要去处理它。

下面是一个导致野指针出现的示例,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint32_t number_1 = 100;
uint32_t* ptr_number = &number_1; // 正确初始化指针
printf("number_1的地址:%" PRIXPTR ",值:%" PRIu32 "\n", (uintptr_t)ptr_number, *ptr_number);

{
// 创建一个新的作用域
uint32_t temp_number = 200; // 局部变量,作用域结束后被销毁
ptr_number = &temp_number; // 指针指向局部变量
printf("temp_number的地址:%" PRIXPTR ",值:%" PRIu32 "\n", (uintptr_t)ptr_number, *ptr_number);
}

// 作用域结束后,ptr_number成为野指针
printf("野指针的地址:%" PRIXPTR ",值:%" PRIu32 "(未定义行为)\n", (uintptr_t)ptr_number, *ptr_number);
// 此处野指针还可以读取到之前的值,是因为栈内存未被覆盖,但这种行为是未定义的,可能导致程序崩溃或读取到垃圾值

11. 数组的首地址与指针的算数运算

可以使用加减法对指针进行运算,从而拿到想要的值,最典型的用法就是在数组中。

由于数组中的所有元素在内存中都是连续的,因此数组中每个元素的地址也是连续的,因此定义一个数组的地址时,直接将数组赋给指针,不需要有取地址符(&),此时这个指针的值就是这个数组的首地址,即数组中第一个元素的地址。例如:

1
2
3
4
5
6
// 数组在内存中是连续存储的
uint32_t numbers[] = { 10, 20 , 30, 40, 50 ,60, 70, 80, 90, 100 };

// 指向数组的第一个元素,即 &numbers[0]
// 不需要使用取地址符号 &,因为数组名本身就是一个指向第一个元素的指针
uint32_t* ptr_numbers = numbers;

可以使用sizeof()函数计算数组的大小:

1
2
3
4
5
// size_t 是用于表示对象大小的无符号整数类型
// sizeof 运算符返回的是字节数
// sizeof(numbers) 是数组的总字节数,sizeof(numbers[0]) 是单个元素的字节数
size_t size = sizeof(numbers) / sizeof(numbers[0]);
printf("数组的大小: %zu\n", size); // zu 是 size_t 类型的格式说明符

指针加减法不会按字节增加,而是按元素大小增加,指针变化1,实际上的地址是变化了sizeof(数组的数据类型)个字节。

指针的加法:

1
2
3
4
5
printf("使用指针加法访问数组元素:\n");
// 指针加法不会按字节增加,而是按元素大小增加
// 指针加 1 实际上是地址增加了 sizeof(uint32_t) 字节
ptr_numbers += 4;
printf("numbers[ptr_numbers += 4] = %" PRIu32 "\n", *ptr_numbers);

指针的减法:

1
2
3
4
printf("使用指针减法访问数组元素:\n");
// 如果此处减去的数超过了数组的起始位置,行为是未定义的
ptr_numbers -= 4;
printf("numbers[ptr_numbers -= 4] = %" PRIu32 "\n", *ptr_numbers);

使用指针减法计算数组距离:

1
2
3
4
uint32_t* start_ptr = &numbers[0];
uint32_t* end_ptr = &numbers[size - 1];
printf("数组起始地址: %" PRIXPTR ",结束地址: %" PRIXPTR "\n", (uintptr_t)start_ptr, (uintptr_t)end_ptr);
printf("数组元素个数 (end_ptr - start_ptr) = %" PRIdPTR "\n", (ptrdiff_t)(end_ptr - start_ptr + 1));

可以使用指针的运算来遍历打印数组中的每个元素:

1
2
3
4
5
6
7
8
printf("数组元素:{ ");
for (size_t i = 0; i < size; i++) {
// 使用指针算数运算访问数组元素
// ptr_numbers + i 指向第 i 个元素
// *(ptr_numbers + i) 解引用指针以获取该元素的值
printf("%" PRIu32 " ", *(ptr_numbers + i));
}
printf("}\n");

12. 指针的算数运算与比较运用

可以直接使用外部指针遍历数组:

1
2
3
4
5
printf("指针遍历数组: { ");
for (uint32_t* p = start_ptr; p <= end_ptr; p++) {
printf("%" PRIu32 " ", *p);
}
printf("}\n");

使用指针实现数组反向遍历:

1
2
3
4
5
printf("反向遍历数组元素: { ");
for (uint32_t* p = end_ptr; p >= start_ptr; p--) {
printf("%" PRIu32 " ", *p);
}
printf("}\n");

使用指针运算访问数组中的特定元素:

1
2
uint32_t offset = 3;
printf("访问数组中第 %" PRIu32 " 个元素: %" PRIu32 "\n", offset, *(start_ptr + offset - 1));

指针之间的比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uint32_t* middle_ptr = start_ptr + size / 2;
if (middle_ptr > start_ptr) {
puts("start_ptr 指向的元素在 middle_ptr 指向的元素之前");
}
else {
puts("start_ptr 指向的元素在 middle_ptr 指向的元素之后");
}

if (middle_ptr < end_ptr) {
puts("middle_ptr 指向的元素在 end_ptr 指向的元素之前");
}
else {
puts("middle_ptr 指向的元素在 end_ptr 指向的元素之后");
}

13. 再探size_t与数组与指针的使用

size_t是一个无符号整数类型,它专门用来表示大小、长度和索引,特别定义这样一种数据类型可以提高程序在不同平台之间跨平台的可移植性和安全性。

1
2
3
4
5
6
uint32_t numbers[] = { 10, 20 , 30, 40, 50 ,60, 70, 80, 90, 100 };
uint32_t* ptr_numbers = numbers;

// 计算数组的大小
size_t size = sizeof(numbers) / sizeof(numbers[0]);
printf("数组的大小: %zu\n", size); // zu 是 size_t 类型的格式说明符

可以使用指针的运算配合指针的解引用来修改数组中元素的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 打印修改前的数组元素
printf("修改前的数组元素:{ ");
for (size_t i = 0; i < size; i++)
{
printf("%" PRIu32 " ", *(ptr_numbers + i));
}
printf("}\n");

// 数组中每一个元素加5
for (size_t i = 0; i < size; i++)
{
*(ptr_numbers + i) += 5;
}
// 打印修改后的数组元素
printf("数组中每一个元素加 5 后的值:{ ");
for (size_t i = 0; i < size; i++)
{
printf("%" PRIu32 " ", *(ptr_numbers + i));
}
printf("}\n");

14. 案例:指针查找特定元素的索引并返回

要求:使用指针计算出一个特定数组中某一个元素的下标索引值并返回

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

// C语言案例
int main(void) {
// 一个包含5个uint32_t类型元素的数组
uint32_t array[] = { 10, 20, 30, 40, 50 };

// 指向数组第一个元素的指针
uint32_t* start_ptr = array;

// 数组的大小
size_t array_size = sizeof(array) / sizeof(array[0]);

// 目标值
uint32_t target_value = 30;

// 指向目标值的指针,初始为NULL
uint32_t* target_ptr = NULL;

// 记录目标值的下标索引,初始为0
size_t index = 0;

// 是否找到目标值的标志
bool found = false;

// 遍历数组,查找目标值
for (size_t i = 0; i < array_size; i++)
{
if (*(start_ptr + i) == target_value)
{
target_ptr = start_ptr + i; // 获取目标值的指针
index = i; // 记录下标索引
found = true; // 设置找到标志
break; // 退出循环
}
}

if (found)
{
printf("元素%" PRIu32 "的下标是%zu\n", target_value, index);
}
else
{
printf("元素%" PRIu32 "未找到!\n", target_value);
}

return 0;
}

我们可以将在数组中寻找某一个元素并返回它的下标索引值的功能整合成一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

// 查找数组中某个元素的下标索引值
bool find_index_of_array(const uint32_t* array, size_t size, uint32_t target_value, size_t* index);

// C语言案例
int main(void) {
// 一个包含5个uint32_t类型元素的数组
uint32_t array[] = { 10, 20, 30, 40, 50 };

// 数组的大小
size_t array_size = sizeof(array) / sizeof(array[0]);

// 目标值
uint32_t target_value = 30;

// 记录目标值的下标索引,初始为0
size_t index = 0;

// 调用函数查找目标值的下标索引
bool found = find_index_of_array(array, array_size, target_value, &index);

if (found)
{
printf("元素%" PRIu32 "的下标是%zu\n", target_value, index);
}
else
{
printf("元素%" PRIu32 "未找到!\n", target_value);
}

return 0;
}

bool find_index_of_array(const uint32_t* array, size_t size, uint32_t target_value, size_t* index) {
for (size_t i = 0; i < size; i++) {
if (*(array + i) == target_value)
{
*index = i;
return true;
}
}
return false;
}

关于如何向函数中传入指针,会在之后几节中学到。

15. 指针访问多维数组

使用指针同样可以访问多维数组,其定义和访问方式需要注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint32_t matrix[3][4] = {
{ 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9, 10, 11, 12}
};

// 定义一个指向包含4个uint32_t元素的一维数组的指针
// 指针指向二维数组的第一行
uint32_t(*ptr)[4] = matrix;

// 通过指针访问二维数组的元素
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 4; j++) {
// 两种方式等价
// 第一种方式更直观地展示了指针运算
// 第二种方式更符合数组访问的习惯,较为常见
//printf("%" PRIu32 " ", *(*(ptr + i) + j));
printf("%" PRIu32 " ", ptr[i][j]);
}
printf("\n");
}

定义二维数组的指针

定义二维数组的指针时,必须同时传入二维数组一行的元素个数,这样在使用指针时可以类似于访问二维数组:

  • 指针的算数在第一层按数组行为单位移动,以上面的数组为例,每次移动时,地址偏移4×sizeof(uint32_t) = 16字节
  • 地址第二层按元素为单位移动

即:

1
2
3
4
5
6
matrix          → uint32_t(*)[4]    // 数组名退化为指向首行的指针
ptr → uint32_t(*)[4] // 显式声明的指针类型
ptr + 1 → uint32_t(*)[4] // 指针算术,类型不变
*(ptr + 1) → uint32_t[4] // 解引用得到一行(数组)
*(ptr + 1) + 2 → uint32_t* // 数组退化为指向首元素的指针,再偏移 2
*(*(ptr + 1) + 2) → uint32_t // 最终解引用得到值(matrix[1][2] = 7)

用指针访问二维数组

用指针访问二维数组时,可以直接使用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
2
uint32_t array[10];   // 数组:分配 40 字节内存(10 × 4 字节)
uint32_t* ptr; // 指针:分配 8 字节(64位系统),仅存储地址

数组取地址

对于取地址运算符&,数组取地址之后得到的是指向整个数组的指针:

1
2
3
4
5
6
7
8
9
10
uint32_t array[10];
uint32_t* ptr = array;

// 类型分析
array → uint32_t* (退化为指向首元素的指针)
&array → uint32_t(*)[10] (指向整个数组的指针)
&array[0] → uint32_t* (指向首元素的指针)

ptr → uint32_t* (指针本身)
&ptr → uint32_t** (指向指针的指针)

array和&array的区别:

  • array和&array的值相同(都是数组首地址),但类型不同
  • &array指向的是"整个数组",+1会跨过整个数组
  • array退化后指向"首元素",+1只跨过一个元素

16. 函数传递数组

一维数组

在C里“向函数传数组”,本质上有几种常见方式。核心先记住一句:

数组作为函数参数时,会退化为指针。

也就是说写int arr[]、int arr[10]、int* arr在形参位置几乎等价(都会变成指向首元素的指针)。

最常见的写法是“数组指针+长度”:

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

调用:

1
2
int a[] = { 1,2,3,4 };
printArray(a, sizeof(a) / sizeof(a[0]));

向函数中传递数组时,必须同时传递数组大小,因为函数参数里的arr已经是指针,sizeof(arr)得到的是指针大小(4/8字节),不是数组总字节数。

还有一种写法是将数组写成arr[]形式,语义上更像数组。

1
void sumArray(const int arr[], size_t len);

这只是写法更直观,底层仍是const int* arr。

二维数组

二维数组作为参数传递时,关键点就一句:列数必须已知(或可推导)。

最常见的写法就是数组固定列数写法,例如:

1
2
3
4
5
6
7
8
void print2D(const int arr[][3], size_t rows) {
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < 3; ++j) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}

调用:

1
2
int m[2][3] = { {1,2,3}, {4,5,6} };
print2D(m, 2);

其等价的指针写法为:

1
void print2D(const int (*arr)[3], size_t rows);

与一维数组不同的是,列数必须固定,因为访问arr[i][j]时,编译器要知道一行有几个元素,才能算出偏移地址。

所以行数可以运行时传,列数必须在类型里固定。

常见错误:

  • 写成void f(int arr[][])(非法)
  • 把int**当成二维数组传递(通常不兼容)
  • 在函数里sizeof(arr)取行列(拿到的是指针大小,不是总大小)

17. 案例:小仓库items管理

案例:写一个用于在小仓库中管理items的程序,用于练习函数传递指针。

需要实现的功能包括数组中结构体的打印,添加与更新数量功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

// 仓库可容纳的最大商品数量
#define MAX_ITEMS 100
// 商品名称最大长度(含字符串结束符)
#define MAX_NAME_LENGTH 50

// 商品结构体
typedef struct {
int32_t id;
char name[MAX_NAME_LENGTH];
int32_t quantity;
float price;
} Item;

// 打印当前库存列表
void print_inventory(Item* inventory, int size);

// 向库存中新增一条商品记录
bool add_item(Item* inventory, int* size, int32_t id, const char* name, int32_t quntity, float price);

// 按商品 ID 更新库存数量
bool update_quantity(Item* inventory, int size, int32_t id, int32_t new_quantity);

// 小仓库items管理
int main() {
Item inventory[MAX_ITEMS];
int32_t size = 0; // 当前库存中已存储的商品数量

// 初始化库存数据
add_item(inventory, &size, 1, "Apple", 50, 0.5f);
add_item(inventory, &size, 2, "Banana", 30, 0.3f);
add_item(inventory, &size, 3, "Orange", 20, 0.4f);

printf("Initial Inventory:\n");
print_inventory(inventory, size);

printf("\nUpdating Banana quantity to 25...\n");
update_quantity(inventory, size, 2, 25); // 更新香蕉的数量
print_inventory(inventory, size);

return EXIT_SUCCESS;
}

// 逐行输出库存信息
void print_inventory(Item* inventory, int size) {
printf("ID\tName\tQuantity\tPrice\n");
for (int i = 0; i < size; i++) {
printf("%" PRId32 "\t%s\t%" PRId32 "\t\t%.2f\n", inventory[i].id, inventory[i].name, inventory[i].quantity, inventory[i].price);
}
}

// 新增商品,成功返回 true,库存已满返回 false
bool add_item(Item* inventory, int* size, int32_t id, const char* name, int32_t quantity, float price) {
if (*size >= MAX_ITEMS) {
return false; // Inventory full
}
inventory[*size].id = id;
strncpy_s(inventory[*size].name, MAX_NAME_LENGTH, name, _TRUNCATE); // _TRUNCATE相当于MAX_NAME_LENGTH-1,是C11标准中的一个宏,用于指示strncpy_s函数在复制字符串时,如果目标缓冲区不足以容纳整个源字符串,则截断源字符串并确保目标缓冲区以空字符结尾。
inventory[*size].quantity = quantity;
inventory[*size].price = price;
(*size)++;
return true;
}

// 根据 ID 查找商品并更新数量
bool update_quantity(Item* inventory, int size, int32_t id, int32_t new_quantity) {
for (int i = 0; i < size; i++) {
if (inventory[i].id == id) {
inventory[i].quantity = new_quantity;
return true; // Item found and updated
}
}
return false; // Item not found
}

18. 指针数组

指针同样可以写成数组,一个指针数组内可以储存多个指针。

将上一节中定义多维数组的指针中uint32_t(*ptr)[4]的括号去掉,即可创建一个指针数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint32_t class_1[] = { 1001, 1002, 1003 };
uint32_t class_2[] = { 2001, 2002, 2003, 2004 };
uint32_t class_3[] = { 3001, 3002 };

// 创建一个指向指针的数组,元素类型为uint32_t*
uint32_t* class_ptrs[] = { class_1, class_2, class_3 };

uint32_t class_sizes[] = {
sizeof(class_1) / sizeof(class_1[0]),
sizeof(class_2) / sizeof(class_2[0]),
sizeof(class_3) / sizeof(class_3[0])
};

for (size_t i = 0; i < 3; i++)
{
printf("class_%zu: ", i + 1);
for (size_t j = 0; j < class_sizes[i]; j++)
{
printf("%" PRIu32 " ", class_ptrs[i][j]);
}
printf("\n");
}

指针数组中指针元素指向数组时,使用指针访问数组中的元素时可以使用类似二维数组的方法。

19. 函数的值传递与地址引用传递

在学到指针之前,声明定义一个函数时,向函数中传入的是一个值,这种方式被称为函数的值传递(Pass by value)。

例如一个实现将一个变量的值加10的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 值传递,将传入的值加10并返回
uint32_t add_ten_by_value(uint32_t value);

// C语言案例
int main(void) {
uint32_t my_value = 5;
printf("初始的值:%" PRIu32 "\n", my_value);

my_value = add_ten_by_value(my_value);

printf("加10后的值:%" PRIu32 "\n", my_value);

return 0;
}

uint32_t add_ten_by_value(uint32_t value) {
return value + 10;
}

可以注意到,由于变量作用域的问题,我们无法在函数中直接修改变量的值,只能将修改后的值返回,并在main函数中实现变量修改。

向函数中传入一个值,相当于将这个值拷贝到这个函数自己的作用域中的一个变量中,在函数中修改这个值,对原值没有影响。

而指针储存了变量在内存中的地址,如果函数拿到了这个地址,就可以实现跨作用域修改变量的值,这种方法被称为地址引用传递(Pass by Reference)。

此时,可以直接在函数中利用指针修改变量值,函数返回值为空即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 地址引用传递,将10加到传入的值上
void add_ten_by_value(uint32_t* value);

// C语言案例
int main(void) {
uint32_t my_value = 5;
printf("初始的值:%" PRIu32 "\n", my_value);

// 向函数传递地址引用
add_ten_by_value(&my_value);

printf("加10后的值:%" PRIu32 "\n", my_value);

return 0;
}

void add_ten_by_value(uint32_t* value) {
// 通过地址引用修改传入的值,实现跨作用域的值修改
*value += 10;
}

使用地址引用传递可以跨函数跨作用域修改操作值,提供特定功能的外部服务。这也是指针通常在函数中使用频繁的原因。

20. 案例:员工薪资系统:指针作为函数返回值

指针可以作为函数的传入值,也可以作为函数的返回值。

要求:制作一个简易的员工薪资管理系统,假设一共有五名员工,需要实现的功能包括:

  • 给所有员工批量涨工资;
  • 可以批量打印所有员工的薪资;
  • 查找最高薪资的员工,并给这名员工奖励5000元;
  • 计算并给所有员工发年终奖(假设年终奖为涨薪最高的员工的薪资除以10)。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

#define EMPLOYEE_COUNT 5 // 员工总数

// 给所有员工批量涨工资
void raise_salaries(uint32_t* salaries, uint32_t raise_amount);

// 批量打印所有员工的薪资
void print_salaries(const uint32_t* salaries);

// 查找最高薪资的员工
uint32_t* find_highest_salary_employee(uint32_t* salaries);

// 计算并给所有员工发年终奖
void distribute_year_end_bonus(uint32_t* salaries);


// C语言案例
int main(void) {
uint32_t salaries[EMPLOYEE_COUNT] = { 3000, 3500, 3200, 4500, 4000 };

printf("初始员工薪资:\n");
print_salaries(salaries);

// 给所有员工涨工资2000元
raise_salaries(salaries, 2000);
printf("\n涨薪后的员工薪资:\n");
print_salaries(salaries);

// 查找最高薪资的员工并奖励5000元
// 直接通过指针修改最高薪资员工的薪资
*find_highest_salary_employee(salaries) += 5000;
printf("\n奖励最高薪资员工后的员工薪资:\n");
print_salaries(salaries);

// 发放年终奖
distribute_year_end_bonus(salaries);
printf("\n发放年终奖后的员工薪资:\n");
print_salaries(salaries);

return 0;
}

// 给所有员工批量涨工资
void raise_salaries(uint32_t* salaries, uint32_t raise_amount) {
for (size_t i = 0; i < EMPLOYEE_COUNT; i++) {
salaries[i] += raise_amount;
}
}

// 批量打印所有员工的薪资
void print_salaries(const uint32_t* salaries) {
for (size_t i = 0; i < EMPLOYEE_COUNT; i++) {
printf("员工 %zu: %" PRIu32 " 元\n", i + 1, salaries[i]);
}
}

// 查找最高薪资的员工
uint32_t* find_highest_salary_employee(uint32_t* salaries) {
uint32_t* highest_ptr = salaries;
for (size_t i = 1; i < EMPLOYEE_COUNT; i++) {
if (salaries[i] > *highest_ptr) {
highest_ptr = &salaries[i];
}
}
// 返回指向最高薪资员工的指针
return highest_ptr;
}

// 计算并给所有员工发年终奖
void distribute_year_end_bonus(uint32_t* salaries) {
// 先找到最高薪资(只查找一次)
uint32_t* highest = find_highest_salary_employee(salaries);
uint32_t bonus = *highest / 10; // 计算年终奖金额
raise_salaries(salaries, bonus); // 给所有员工发放年终奖
}

在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); // 最终都变成这个

写成数组的形式易于阅读,写成指针的形式更强调这个变量是一个指针,用哪种都可以。

21. 游戏案例:指针的练习

要求:制作一个简单的游戏系统,包含以下功能:

  • 增加经验:模拟玩家完成一些游戏任务后增加经验;
  • 提升等级:每当玩家达到100经验值时可以升一级,最高十级;
  • 获得宝藏提示:每当玩家成功升一级,可以获得一个每级固定的与宝藏相关的提示,一共十条提示。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

// 游戏系统常量
#define EXP_PER_LEVEL 100 // 每级所需经验值
#define MAX_LEVEL 10 // 最高等级
#define TOTAL_TIPS 10 // 宝藏提示总数

// 宝藏提示数组
// static表示仅在本文件内可见,const表示内容不可修改
static const char* treasure_tips[TOTAL_TIPS] = {
"提示 1: 宝藏在古老的树下。",
"提示 2: 寻找隐藏在岩石后的洞穴。",
"提示 3: 宝藏埋藏在沙漠的绿洲中。",
"提示 4: 在废弃的城堡里寻找线索。",
"提示 5: 宝藏藏在河流的尽头。",
"提示 6: 寻找山顶上的古代遗迹。",
"提示 7: 宝藏隐藏在密林深处。",
"提示 8: 在海滩的岩石间寻找。",
"提示 9: 宝藏埋藏在火山口附近。",
"提示 10: 寻找天空之城的秘密宝藏。"
};

// 增加经验
void increase_exp(uint32_t* exp, uint32_t amount);

// 提升等级
bool level_up(uint32_t* exp, uint32_t* level);

// 获得宝藏提示
const char* get_treasure_tip(uint32_t level);

// 打印玩家状态
void print_player_status(uint32_t exp, uint32_t level);

// C语言案例
int main(void) {
uint32_t player_exp = 0; // 玩家经验值
uint32_t player_level = 0; // 玩家等级

// 打印玩家初始状态
printf("游戏开始,玩家初始状态:\n");
print_player_status(player_exp, player_level);

// 模拟游戏过程,玩家完成任务并增加经验
increase_exp(&player_exp, 50); // 模拟增加50经验
increase_exp(&player_exp, 70); // 模拟增加70经验

// 尝试升级
if (level_up(&player_exp, &player_level)) {
printf("\n升级后玩家状态:\n");
print_player_status(player_exp, player_level);
}

// 游戏结束
printf("\n游戏结束,感谢游玩!\n");

return 0;
}

// 增加经验
void increase_exp(uint32_t* exp, uint32_t amount) {
*exp += amount;
printf("增加经验: %" PRIu32 ",当前经验: %" PRIu32 "\n", amount, *exp);
}

// 提升等级
bool level_up(uint32_t* exp, uint32_t* level) {
// 检查是否已达最高等级
if (*level >= MAX_LEVEL) {
if (*exp >= EXP_PER_LEVEL) {
printf("已达到最高等级 %u,经验不再计入。\n", MAX_LEVEL);
*exp = 0; // 清空多余经验
}
return false;
}

// 检查是否有足够经验升级
if (*exp >= EXP_PER_LEVEL) {
printf("\n升级啦!\n");
while (*exp >= EXP_PER_LEVEL && *level < MAX_LEVEL) {
*exp -= EXP_PER_LEVEL;
(*level)++;
printf("恭喜!你已升级到等级 %" PRIu32 "。\n", *level);
printf("获得宝藏提示:%s\n", get_treasure_tip(*level));
}
// 如果升到满级后还有剩余经验
if (*level >= MAX_LEVEL && *exp > 0) {
printf("已达到最高等级,剩余经验已清空。\n");
*exp = 0;
}
return true;
} else {
printf("\n经验不足,无法升级。\n");
return false;
}
}

// 获得宝藏提示
const char* get_treasure_tip(uint32_t level) {
if (level == 0 || level > TOTAL_TIPS) {
return "无提示可用。";
}
return treasure_tips[level - 1];
}

// 打印玩家状态
void print_player_status(uint32_t exp, uint32_t level) {
printf("当前经验值: %" PRIu32 "\n", exp);
printf("当前等级: %" PRIu32 "\n", level);
}

关于使用char*定义字符串的内容,会在后面字符串的章节学到。

22. 练习:更新分数

要求:有两名游戏玩家,需要对他们的游戏得分进行一些操作:

  • 改变(增加或减少)某位玩家的分数;
  • 比较两位玩家的分数,拿到较高分数的玩家的得分;
  • 让某一位玩家的分数变为两倍。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

// 改变玩家分数
void change_score(int32_t* score, int32_t delta);

// 比较两位玩家的分数,返回较高分数
int32_t* compare_scores(int32_t* score1, int32_t* score2);

// 让某一位玩家的分数变为两倍
void double_score(int32_t* score);

// C语言案例
int main(void) {
int32_t player1_score = 50;
int32_t player2_score = 75;

printf("初始分数:玩家1 = %" PRId32 ", 玩家2 = %" PRId32 "\n", player1_score, player2_score);

// 改变玩家的分数
change_score(&player1_score, 20);
change_score(&player2_score, -10);
printf("改变后分数:玩家1 = %" PRId32 ", 玩家2 = %" PRId32 "\n", player1_score, player2_score);

// 比较两位玩家的分数
printf("较高分数的玩家得分 = %" PRId32 "\n", *compare_scores(&player1_score, &player2_score));

// 让某一位玩家的分数变为两倍
double_score(&player1_score);
printf("玩家1分数变为两倍后 = %" PRId32 "\n", player1_score);

return 0;
}

// 改变玩家分数
void change_score(int32_t* score, int32_t delta) {
*score += delta;
}

// 比较两位玩家的分数,返回较高分数
int32_t* compare_scores(int32_t* score1, int32_t* score2) {
return (*score1 > *score2) ? score1 : score2;
}

// 让某一位玩家的分数变为两倍
void double_score(int32_t* score) {
*score *= 2;
}

23. 游戏案例:收藏奖杯

要求:一个游戏,可以达成一些成就,最多能获得十个成就,需要有以下功能:

  • 增加成就数量,将达成的成就储存下来;
  • 打印当前已获得的所有成就。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

#define MAX_ACHIEVEMENTS 10 // 最大成就数量

const char* achievements[MAX_ACHIEVEMENTS]; // 成就数组

size_t achievement_count = 0; // 当前成就数量

// 增加成就
void add_achievement(const char* achievement);

// 打印所有成就
void print_achievements();

// C语言案例
int main(void) {
add_achievement("完成了新手教程");
add_achievement("击败了第一个Boss");
add_achievement("收集了所有隐藏物品");
print_achievements();

return 0;
}

// 增加成就
void add_achievement(const char* achievement) {
if (achievement_count < MAX_ACHIEVEMENTS) {
achievements[achievement_count++] = achievement; // 先存储成就,然后成就计数加一
printf("成就已添加: %s\n", achievement);
}
else
{
printf("成就数量已达上限,无法添加更多成就。\n");
}
}

// 打印所有成就
void print_achievements() {
printf("当前已获得的成就:\n");
for (size_t i = 0; i < achievement_count; i++) {
printf("%zu. %s\n", i + 1, achievements[i]);
}
}

24. 第八章结束语

至此,指针的基础知识已介绍结束。

其他有关指针的深入使用,例如:指针操作字符串、指针在结构体中的应用、内存的动态分配、多级指针等内容,将在后续章节中介绍。

本文来源: 青江的个人站
本文链接: https://hanqingjiang.com/2025/12/24/20251224_C_pointer/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
知识共享许可协议
赏

谢谢你请我喝可乐~

支付宝
微信
  • Notes
  • C

扫一扫,分享到微信

微信分享二维码
【C语言学习笔记】九、结构体
【C语言学习笔记】七、函数
  1. 1. 1. 地址
  2. 2. 2. 取地址的含义
  3. 3. 3. 指针
  4. 4. 4. 指针与修改
  5. 5. 5. 指针星号的企业风格规范以及容易引发的问题
  6. 6. 6. 指针的意义与作用究竟是什么:外部服务操作
  7. 7. 7. 野指针初步介绍
  8. 8. 8. 空指针初步介绍
  9. 9. 9. 空指针的初始化
  10. 10. 10. 从代码上尝试认识野指针
  11. 11. 11. 数组的首地址与指针的算数运算
  12. 12. 12. 指针的算数运算与比较运用
  13. 13. 13. 再探size_t与数组与指针的使用
  14. 14. 14. 案例:指针查找特定元素的索引并返回
  15. 15. 15. 指针访问多维数组
    1. 15.1. 定义二维数组的指针
    2. 15.2. 用指针访问二维数组
    3. 15.3. 数组和指针的区别
    4. 15.4. 数组取地址
  16. 16. 16. 函数传递数组
    1. 16.1. 一维数组
    2. 16.2. 二维数组
  17. 17. 17. 案例:小仓库items管理
  18. 18. 18. 指针数组
  19. 19. 19. 函数的值传递与地址引用传递
  20. 20. 20. 案例:员工薪资系统:指针作为函数返回值
  21. 21. 21. 游戏案例:指针的练习
  22. 22. 22. 练习:更新分数
  23. 23. 23. 游戏案例:收藏奖杯
  24. 24. 24. 第八章结束语
© 2021-2026 青江的个人站
晋ICP备2024051277号-1
powered by Hexo & Yilia
  • 友链
  • 搜索文章 >>

tag:

  • 生日快乐🎂
  • 新年快乐!
  • 小技巧
  • Linux
  • 命令
  • 语录
  • 复刻
  • Blog
  • Notes
  • Android
  • C
  • Homework
  • MATLAB
  • Server
  • FPGA
  • Vivado
  • Git

  • 引路人-稚晖
  • Bilibili-稚晖君
  • 超有趣讲师-Frank
  • Bilibili-Frank