使用指针遍历数组
在 C 和 C++ 中,数组名在很多情况下会隐式地“退化”(decay)为指向其首元素的指针。利用这一点以及指针算术(pointer arithmetic),我们可以遍历数组。
核心概念:
-
数组名与指针:
int arr[5];声明了一个包含 5 个整数的数组。表达式arr本身可以被用作一个指向arr[0]的指针,即arr的类型可以隐式转换为int*,其值是数组第一个元素的内存地址。 -
指针算术: 对指向数组元素的指针进行算术运算(如
+,-,++,--)时,编译器会自动考虑所指向元素类型的大小。例如,如果ptr是一个int*,ptr++会将ptr的值增加sizeof(int)个字节,使其指向内存中的下一个int元素。 -
解引用: 使用解引用运算符
*可以获取指针所指向地址处存储的值。*ptr会得到ptr当前指向的那个数组元素的值。 -
确定结束位置: 这是指针遍历的关键。你需要知道何时停止。通常的方法是获取一个指向数组末尾之后一个位置的指针(one-past-the-end pointer)。对于数组
arr和其大小size,这个结束指针可以通过arr + size计算得到。注意,arr + size指向的是最后一个元素之后紧邻的内存地址,不应该解引用这个结束指针,它仅用作循环的边界条件。
遍历方法:
通常使用 for 或 while 循环。
-
初始化: 创建一个指针,并使其指向数组的第一个元素。
int arr[] = {10, 20, 30, 40, 50}; size_t size = sizeof(arr) / sizeof(arr[0]); int* ptr = arr; // 或者 int* ptr = &arr[0]; -
结束条件: 计算指向数组末尾之后一个位置的指针。
int* end_ptr = arr + size; // 或者 int* end_ptr = &arr[size]; -
循环: 循环继续的条件是当前指针
ptr还没有到达结束指针end_ptr。在循环内部,先解引用ptr来访问当前元素,然后递增ptr以移向下一个元素。
示例代码 (使用 while 循环):
#include <iostream>
#include <cstddef> // for size_t
int main() {
int numbers[] = {5, 15, 25, 35, 45};
size_t count = sizeof(numbers) / sizeof(numbers[0]);
int* current_ptr = numbers; // 1. 指针指向数组开头
int* end_ptr = numbers + count; // 2. 获取指向末尾后一个位置的指针
std::cout << "Array elements using while loop and pointers: ";
while (current_ptr != end_ptr) { // 3. 循环直到当前指针到达结束指针
// 4. 解引用指针(*)获取当前元素的值
std::cout << *current_ptr << " ";
// 5. 指针向前移动到下一个元素
current_ptr++;
}
std::cout << std::endl;
return 0;
}
示例代码 (使用 for 循环):
for 循环可以将初始化、条件和递增更紧凑地写在一起:
#include <iostream>
#include <cstddef>
int main() {
int scores[] = {100, 90, 80, 70, 60};
size_t num_scores = sizeof(scores) / sizeof(scores[0]);
int* end_scores = scores + num_scores;
std::cout << "Array elements using for loop and pointers: ";
// 初始化: ptr = scores
// 条件: ptr != end_scores
// 递增: ++ptr (在每次循环体执行后)
for (int* ptr = scores; ptr != end_scores; ++ptr) {
std::cout << *ptr << " "; // 解引用访问元素
}
std::cout << std::endl;
// 另一种 for 循环形式 (结合索引和指针算术)
std::cout << "Array elements using for loop (index + pointer arithmetic): ";
for (size_t i = 0; i < num_scores; ++i) {
// *(scores + i) 等价于 scores[i]
// scores + i 计算出第 i 个元素的地址
// * 取该地址处的值
std::cout << *(scores + i) << " ";
}
std::cout << std::endl;
return 0;
}
优点与缺点:
-
优点:
- 在某些底层操作或与 C 代码交互时可能更自然。
- 对于理解内存布局和指针工作原理非常有帮助。
- 在某些非常注重性能的旧代码或特定场景下,可能被认为(有时是错误地)稍微高效一点(现代编译器优化通常能抹平差异)。
-
缺点:
- 更容易出错: 指针操作(如算术、解引用、边界检查)如果处理不当,极易导致悬挂指针、访问越界等问题,引发未定义行为和程序崩溃。
-
可读性较差: 相较于索引
arr[i]或范围for,指针遍历通常更难理解和维护。 -
安全性低: C++ 提供了更安全、更现代的替代方案(
std::vector,std::array, 范围for)。
现代 C++ 建议:
在现代 C++ 中,除非有非常特殊的原因(例如性能分析表明确实有显著差异,或与 C API 交互),通常不推荐优先使用指针遍历数组。应首选范围 for 循环,其次是索引 for 循环,或者使用标准库容器 (std::vector, std::array) 配合它们的成员函数和迭代器。
我们之前玩过按编号看盒子(索引循环)和神奇袋子自动给玩具(范围 for 循环)的游戏。今天我们来玩一个“寻宝探险”游戏,我们要用一个特别的“魔法指示棒”来找到藏在一排石头下面的所有宝藏!这个指示棒就是电脑里的“指针”。
-
准备材料:
- 几张纸或者几个垫子放在地上排成一条直线,当作“石板路”。
- 在每块“石板”下面藏一个“宝藏”(小玩具、图片卡片、糖果等)。
- 一根真实的、可以指向东西的棒子(比如教鞭、筷子、或者就是一个手指),作为我们的“魔法指示棒”(指针)。
- 在石板路的终点(最后一块石板的后面)放一个明显的标记物,比如一个红色的圆圈或者一个小旗子,表示“探险终点”。
-
动手操作:
- 老师/家长: “看!这是一条神秘的石板路,每块石板下面都可能藏着宝藏!我们要用这根‘魔法指示棒’来找到它们。这个指示棒很特别,它不认识石板的编号,但它知道怎么指向第一块石板,并且知道怎么跳到下一块石板。”
-
执行任务:
- “首先,让我们的魔法指示棒指向第一块石板。” (把棒子指向第一块石板)。
- “好,指示棒指着这里。我们看看这块石板下面有什么宝藏?” (拿起棒子指向的石板下面的宝藏)。“哇,找到了第一个宝藏!”
- “现在,命令指示棒:‘跳到下一块石板!’” (把棒子移动到指向第二块石板)。
- “指示棒现在指着第二块了。我们看看下面有什么?” (找到第二个宝藏)。“太棒了!”
- “继续!‘跳到下一块石板!’” (移动棒子到第三块)。“看看有什么?” (找到第三个宝藏)。
- (重复这个过程,直到指示棒指向最后一块有宝藏的石板,并找到宝藏)。
- “好的,我们找到了最后一块石板下的宝藏。现在,再命令指示棒:‘跳到下一块石板!’” (把棒子移动到指向那个‘探险终点’标记物)。
- “指示棒现在指到哪里了?” (指向终点标记)。“这里还有石板和宝藏吗?” (没有)。“对!指示棒到达了我们设置的‘终点标记’,说明我们已经找完了所有石板下的宝藏,探险结束!”
-
关键点强调:
- 我们有一个“路径”(石板路,代表数组)。
- 我们用一个“指示棒”(指针)来标记我们当前在哪块石板。
- 我们通过查看指示棒指向的地方来找到宝藏(解引用
*ptr)。 - 我们通过命令指示棒“跳到下一个”来移动到下一块石板(指针递增
ptr++)。 - 我们需要一个明确的“终点标记”(end pointer),告诉我们什么时候所有石板都检查完了,探险应该停止。
我们把这个过程画下来:
-
准备材料:
- 白板或大张纸。
- 彩色笔。
-
画图演示:
- 老师/家长: 在白板上画一排方块,代表石板路。在每个方块里画上不同的宝藏图案(星星、钻石、金币等)。
- 画一个大大的箭头(我们的“魔法指示棒”),让它的尖端指向第一个方块(第一块石板)。
- 在最后一个方块的后面,画一个特殊的标记(比如一个红叉 X 或者一面小旗),表示“终点”。
- “看,这是我们的藏宝图!箭头指示棒现在指向第一块石板 [指向第一个方块]。我们要看看它指的地方有什么宝藏(星星)。” [可以在箭头旁边画一只眼睛看着星星]
- “然后,让箭头跳到下一块石板。” [擦掉箭头,重新画,使其指向第二个方块]。 “现在它指着第二块了,我们看看有什么宝藏(钻石)。”
- “箭头继续跳...” [重复画箭头跳到下一个方块,并指出宝藏的过程,直到最后一个有宝藏的方块]。
- “找到最后一个宝藏(金币)了!现在,箭头再跳一次...” [擦掉箭头,重新画,使其指向那个红叉 X 或小旗]。
- “箭头现在指到‘终点标记’了!我们知道,不能再往前走了,因为所有的宝藏都已经找到了!”
-
连接概念:
- “这一排方块就是电脑里的数组。”
- “这个箭头就是电脑里的指针 (Pointer),它存储了当前方块的‘地址’(位置)。”
- “箭头指向的地方里面的宝藏,就是通过‘解引用’(用
*号)得到的值。” - “让箭头跳到下一个方块,就是‘指针增加’(用
++号)。” - “那个终点标记,就是一个特殊的‘结束指针’,用来判断我们的寻宝(循环)是否该结束了。”
理解规则:
-
联系规则:
- 老师/家长: “我们要给电脑非常精确的指令,让它用‘指针’来寻宝。规则是这样的:”
- “规则 1:创建一个‘指针’(我们就叫它
ptr),让它指向宝藏路径的起点(第一个方块的地址)。” (写下ptr = 指向第一个宝藏) - “规则 2:找到宝藏路径的终点在哪里(就是最后一个宝藏后面的那个位置),记住这个位置,叫它
end_marker。” (写下end_marker = 指向最后一个宝藏之后的位置) - “规则 3:只要我们的指针
ptr还没有走到end_marker那个位置...” (写下只要 ptr 不等于 end_marker) - “规则 4:...我们就做两件事:”
- “第一件:看看指针
ptr当前正指向的那个地方(*ptr)藏着什么宝藏,并把它拿出来。” (写下查看 *ptr 的宝藏) —— “记住那个星号*的意思是‘看指针指的地方里面是啥’。” - “第二件:让指针
ptr跳到下一个宝藏的位置 (ptr++)。” (写下让 ptr 跳到下一个 (ptr++)) —— “++的意思就是‘跳到下一个’。”
- “第一件:看看指针
- “规则 5:当指针
ptr终于走到了end_marker的位置时,这个‘只要’条件就不满足了,我们的寻宝(循环)就结束了。”
-
展示简化 C++ 代码结构 (概念性):
// treasures 是我们的宝藏列表 (数组) 指针 ptr = 指向 treasures 的开头; 指针 end_marker = 指向 treasures 的结尾之后; // 终点标记 // 开始寻宝循环! while ( ptr != end_marker ) { // 只要还没走到终点标记... // 看看当前指针指的地方是啥宝藏 宝藏 current_treasure = *ptr; // 用 * 号拿出宝藏 print(current_treasure); // 展示我们找到的宝藏 // 让指针跳到下一个宝藏的位置 ptr++; } // 当 ptr 等于 end_marker 时,循环停止,寻宝结束! -
解释:
-
指针 ptr = ...: 我们先设置好指示棒,让它指向起点。 -
指针 end_marker = ...: 我们也标记好终点在哪里。 -
while ( ptr != end_marker ): 这是循环的条件,就像问:“指示棒到终点了吗?” 如果没到,就继续做{}里面的事。 -
*ptr: 这个星号*非常重要,它是“寻宝”的关键!它的意思是“给我看指针ptr现在指着的那个位置里面装的东西!” -
ptr++: 这个是让指示棒“跳到下一个位置”的命令。 - “所以,电脑会一直重复:看宝藏 (
*ptr) -> 跳一步 (ptr++) -> 检查是否到终点,直到走到终点为止。”
-
总结复习:
“今天我们学会了用‘魔法指示棒’(指针)来玩寻宝游戏!我们让指示棒从起点开始,用星号 * 来看它指的地方有什么宝藏,用 ++ 让它跳到下一个地方,一直走到我们设置的终点标记才停下来,这样也能找到所有宝藏。
扩展
在C++中,指针是一种存储内存地址的变量,可以用来高效地遍历数组。使用指针遍历数组是C++编程中的一项基本技术,源自C语言传统。
基本概念
数组在内存中是连续存储的,数组名本身就是指向数组第一个元素的指针。指针可以通过递增操作移动到下一个元素,这使得它们特别适合用于数组遍历。
使用指针遍历数组的方法
基本指针遍历
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // 指针指向数组的第一个元素
for (int i = 0; i < 5; i++) {
std::cout << *ptr << " "; // 解引用指针获取元素值
ptr++; // 移动指针到下一个元素
}
// 输出: 10 20 30 40 50
使用指针比较作为循环条件
int arr[5] = {10, 20, 30, 40, 50};
int* begin = arr; // 指向数组开始
int* end = arr + 5; // 指向数组结束后的位置
for (int* ptr = begin; ptr < end; ptr++) {
std::cout << *ptr << " ";
}
// 输出: 10 20 30 40 50
指针算术操作
int arr[5] = {10, 20, 30, 40, 50};
// 使用指针直接访问元素
std::cout << *(arr + 0) << " "; // 等价于 arr[0]
std::cout << *(arr + 1) << " "; // 等价于 arr[1]
std::cout << *(arr + 2) << " "; // 等价于 arr[2]
指针和下标的关系
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;
// 以下两种访问方式等价
std::cout << arr[2] << " "; // 使用数组下标
std::cout << *(ptr + 2) << " "; // 使用指针算术
使用const指针进行只读遍历
int arr[5] = {10, 20, 30, 40, 50};
const int* ptr = arr; // 指向const int的指针,不能通过该指针修改元素
for (int i = 0; i < 5; i++) {
std::cout << *ptr << " ";
ptr++;
}
技术细节
指针步进:当指针自增时,它实际上增加的是sizeof(元素类型)个字节,而不是简单地加1个字节。
指针边界:在使用指针遍历数组时,必须确保不会越界访问内存。
效率考虑:使用指针遍历通常比使用索引更高效,因为它减少了地址计算。
多维数组:对于多维数组,指针遍历会更复杂,但原理相同。
指针安全:使用指针时要特别注意不要访问已释放或无效的内存区域。