C++(四十六):使用指针遍历数组

使用指针遍历数组

在 C 和 C++ 中,数组名在很多情况下会隐式地“退化”(decay)为指向其首元素的指针。利用这一点以及指针算术(pointer arithmetic),我们可以遍历数组。

核心概念:

  1. 数组名与指针: int arr[5]; 声明了一个包含 5 个整数的数组。表达式 arr 本身可以被用作一个指向 arr[0] 的指针,即 arr 的类型可以隐式转换为 int*,其值是数组第一个元素的内存地址。
  2. 指针算术: 对指向数组元素的指针进行算术运算(如 +, -, ++, --)时,编译器会自动考虑所指向元素类型的大小。例如,如果 ptr 是一个 int*ptr++ 会将 ptr 的值增加 sizeof(int) 个字节,使其指向内存中的下一个 int 元素。
  3. 解引用: 使用解引用运算符 * 可以获取指针所指向地址处存储的值。*ptr 会得到 ptr 当前指向的那个数组元素的值。
  4. 确定结束位置: 这是指针遍历的关键。你需要知道何时停止。通常的方法是获取一个指向数组末尾之后一个位置的指针(one-past-the-end pointer)。对于数组 arr 和其大小 size,这个结束指针可以通过 arr + size 计算得到。注意,arr + size 指向的是最后一个元素之后紧邻的内存地址,不应该解引用这个结束指针,它仅用作循环的边界条件。

遍历方法:

通常使用 forwhile 循环。

  • 初始化: 创建一个指针,并使其指向数组的第一个元素。
    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 循环)的游戏。今天我们来玩一个“寻宝探险”游戏,我们要用一个特别的“魔法指示棒”来找到藏在一排石头下面的所有宝藏!这个指示棒就是电脑里的“指针”。

  1. 准备材料:

    • 几张纸或者几个垫子放在地上排成一条直线,当作“石板路”。
    • 在每块“石板”下面藏一个“宝藏”(小玩具、图片卡片、糖果等)。
    • 一根真实的、可以指向东西的棒子(比如教鞭、筷子、或者就是一个手指),作为我们的“魔法指示棒”(指针)。
    • 在石板路的终点(最后一块石板的后面)放一个明显的标记物,比如一个红色的圆圈或者一个小旗子,表示“探险终点”。
  2. 动手操作:

    • 老师/家长: “看!这是一条神秘的石板路,每块石板下面都可能藏着宝藏!我们要用这根‘魔法指示棒’来找到它们。这个指示棒很特别,它不认识石板的编号,但它知道怎么指向第一块石板,并且知道怎么跳到下一块石板。”
    • 执行任务:
      1. “首先,让我们的魔法指示棒指向第一块石板。” (把棒子指向第一块石板)。
      2. “好,指示棒指着这里。我们看看这块石板下面有什么宝藏?” (拿起棒子指向的石板下面的宝藏)。“哇,找到了第一个宝藏!”
      3. “现在,命令指示棒:‘跳到下一块石板!’” (把棒子移动到指向第二块石板)。
      4. “指示棒现在指着第二块了。我们看看下面有什么?” (找到第二个宝藏)。“太棒了!”
      5. “继续!‘跳到下一块石板!’” (移动棒子到第三块)。“看看有什么?” (找到第三个宝藏)。
      6. (重复这个过程,直到指示棒指向最后一块有宝藏的石板,并找到宝藏)。
      7. “好的,我们找到了最后一块石板下的宝藏。现在,再命令指示棒:‘跳到下一块石板!’” (把棒子移动到指向那个‘探险终点’标记物)。
      8. “指示棒现在指到哪里了?” (指向终点标记)。“这里还有石板和宝藏吗?” (没有)。“对!指示棒到达了我们设置的‘终点标记’,说明我们已经找完了所有石板下的宝藏,探险结束!”
  3. 关键点强调:

    • 我们有一个“路径”(石板路,代表数组)。
    • 我们用一个“指示棒”(指针)来标记我们当前在哪块石板。
    • 我们通过查看指示棒指向的地方来找到宝藏(解引用 *ptr)。
    • 我们通过命令指示棒“跳到下一个”来移动到下一块石板(指针递增 ptr++)。
    • 我们需要一个明确的“终点标记”(end pointer),告诉我们什么时候所有石板都检查完了,探险应该停止。

我们把这个过程画下来:

  1. 准备材料:

    • 白板或大张纸。
    • 彩色笔。
  2. 画图演示:

    • 老师/家长: 在白板上画一排方块,代表石板路。在每个方块里画上不同的宝藏图案(星星、钻石、金币等)。
    • 画一个大大的箭头(我们的“魔法指示棒”),让它的尖端指向第一个方块(第一块石板)。
    • 在最后一个方块的后面,画一个特殊的标记(比如一个红叉 X 或者一面小旗),表示“终点”。
    • “看,这是我们的藏宝图!箭头指示棒现在指向第一块石板 [指向第一个方块]。我们要看看它指的地方有什么宝藏(星星)。” [可以在箭头旁边画一只眼睛看着星星]
    • “然后,让箭头到下一块石板。” [擦掉箭头,重新画,使其指向第二个方块]。 “现在它指着第二块了,我们看看有什么宝藏(钻石)。”
    • “箭头继续...” [重复画箭头跳到下一个方块,并指出宝藏的过程,直到最后一个有宝藏的方块]。
    • “找到最后一个宝藏(金币)了!现在,箭头再一次...” [擦掉箭头,重新画,使其指向那个红叉 X 或小旗]。
    • “箭头现在指到‘终点标记’了!我们知道,不能再往前走了,因为所有的宝藏都已经找到了!”
  3. 连接概念:

    • “这一排方块就是电脑里的数组。”
    • “这个箭头就是电脑里的指针 (Pointer),它存储了当前方块的‘地址’(位置)。”
    • “箭头指向的地方里面的宝藏,就是通过‘解引用’(用 * 号)得到的。”
    • “让箭头跳到下一个方块,就是‘指针增加’(用 ++ 号)。”
    • “那个终点标记,就是一个特殊的‘结束指针’,用来判断我们的寻宝(循环)是否该结束了。”

理解规则:

  1. 联系规则:

    • 老师/家长: “我们要给电脑非常精确的指令,让它用‘指针’来寻宝。规则是这样的:”
    • “规则 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 的位置时,这个‘只要’条件就不满足了,我们的寻宝(循环)就结束了。”
  2. 展示简化 C++ 代码结构 (概念性):

    // treasures 是我们的宝藏列表 (数组)
    指针 ptr = 指向 treasures 的开头; 
    指针 end_marker = 指向 treasures 的结尾之后; // 终点标记
    
    // 开始寻宝循环!
    while ( ptr != end_marker ) { // 只要还没走到终点标记...
        
        // 看看当前指针指的地方是啥宝藏
        宝藏 current_treasure = *ptr; // 用 * 号拿出宝藏
        print(current_treasure);      // 展示我们找到的宝藏
    
        // 让指针跳到下一个宝藏的位置
        ptr++; 
    }
    // 当 ptr 等于 end_marker 时,循环停止,寻宝结束!
    
  3. 解释:

    • 指针 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++;
}

技术细节

  1. 指针步进:当指针自增时,它实际上增加的是sizeof(元素类型)个字节,而不是简单地加1个字节。

  2. 指针边界:在使用指针遍历数组时,必须确保不会越界访问内存。

  3. 效率考虑:使用指针遍历通常比使用索引更高效,因为它减少了地址计算。

  4. 多维数组:对于多维数组,指针遍历会更复杂,但原理相同。

  5. 指针安全:使用指针时要特别注意不要访问已释放或无效的内存区域。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容