CC++ & Algorithm

指针与数组

较难3
语言版本:C++
概述:数组名就是数组第一个元素的地址,指针可以像数组一样用下标访问,也可以移动指针就像翻书。

指针与数组:像翻书一样操作数组里的数据

你有没有想过,数组名其实就是一个“书签”,标记着数组的第一个元素的位置?指针就是可以移动的书签,帮你翻到数组的任意一页。用指针操作数组,比用下标更灵活,也更接近计算机底层的工作方式。

数组名就是第一个元素的地址

在C++中,数组名本质上是一个常量指针,它存储的是数组第一个元素的内存地址。你可以把它想象成书的目录,写着“第1页在位置X”,而 arr[0] 就是那个页面的内容。

下面这段代码验证了这一点:

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {10, 20, 30, 40, 50};  // 定义一个包含5个整数的数组
    
    // 数组名 arr 就是 &arr[0]
    cout << "arr 的值:" << arr << endl;            // 输出地址,比如 0x61fdf0
    cout << "&arr[0] 的值:" << &arr[0] << endl;    // 输出同样的地址
    cout << "arr[0] 的值:" << *arr << endl;        // 用 * 解引用得到第一个元素 10
    
    // 用指针遍历数组
    int* p = arr;   // 指针p指向数组开头,就像把书签夹在第1页
    for (int i = 0; i < 5; i++) {
        cout << "arr[" << i << "] = " << *(p + i) << endl;  // 移动书签翻到第i页
        // 也可以写成 p[i] ,和数组下标一样
    }
    
    // 也可以移动指针(像翻书一样一页一页翻)
    p = arr;        // 重新把书签夹回开头
    for (int i = 0; i < 5; i++) {
        cout << *p << " ";   // 读当前页的内容
        p++;                 // 书签翻到下一页
    }
    cout << endl;
    
    return 0;
}

指针加减:翻页而不是翻字节

当你对指针做加法时,比如 p + 1,它并不是在地址上简单地加1个字节,而是跳到下一个数组元素。为什么?因为指针的类型告诉计算机每个元素占多少字节。

  • 如果 pint*,那么 p + 1 实际地址增加 sizeof(int) = 4字节。
  • 如果 pdouble*,那么 p + 1 实际地址增加 sizeof(double) = 8字节。
  • 如果 pchar*,那么 p + 1 实际地址增加 sizeof(char) = 1字节。

想象你有一本漫画书,每页一张图。用手指着第1页,然后移动手指到第2页,你跳过了整页,而不是只移动1毫米。指针加减元素也是同样的道理,它总是跳到下一个完整元素。

生活例子:排队买零食,每个人站一个格子。队伍的首位同学就是 arr[0],指针 p 指向他。你数“第1个、第2个……”时,每次移动一个格子(一个人),而不是移动1厘米。p + 2 就是指向第3个同学(从0开始计数)。

用指针遍历数组的两种方式

  1. 固定指针 + 偏移量:指针本身不动,用 *(p + i)p[i] 访问第i个元素。
  2. 移动指针:让指针变量 p 不断自增 p++,每次指向下一个元素。

两种方式都行,看你的习惯。第一种像拿着书签不动,只翻页数;第二种像每次翻一页,书签跟着走。

小提示p[i]*(p + i) 在C++里完全等价,编译器会做相同的处理。所以你可以把指针当成数组名来用。

新手容易掉进的坑

  1. 越界访问:指针可以指向数组之外的内存,但那里存着未知数据。比如:

    int arr[3] = {1, 2, 3};
    int* p = arr;
    cout << *(p + 100);  // 危险!跳到数组外面,可能读到垃圾值甚至让程序崩溃
    

    就像你翻书翻到了封面外面,那里什么都没有,甚至可能撞到墙。

  2. 数组名不能修改:数组名是常量指针,你不能写 arr++arr = p。但你可以用另一个指针变量来移动:

    int arr[3] = {1, 2, 3};
    // arr++;     // 错误!数组名不是变量
    int* p = arr;
    p++;           // 正确,p是指针变量
    
  3. 把指针和整数混淆p + 1(int)p + 1 完全不同。前者是元素地址加一个元素大小,后者是地址的整数值加1字节。不要弄混。

  4. 忘记解引用:想输出元素内容时,要用 *p 而不是 pp 输出的是地址,不是数值。

完整示例:用指针统计考试成绩

假设你班有5个同学,考试成绩分别是85, 92, 78, 90, 88。用指针来求总分和平均分。

#include <iostream>
using namespace std;

int main() {
    int scores[5] = {85, 92, 78, 90, 88};   // 五名同学的成绩
    int* ptr = scores;                      // 指针指向数组开头
    int total = 0;                          // 总分初始化0
    
    // 方法1:用指针移动累加
    for (int i = 0; i < 5; i++) {
        total += *ptr;   // 加上当前指针指向的成绩
        ptr++;           // 指针移到下一个同学
    }
    cout << "总分:" << total << endl;
    cout << "平均分:" << total / 5.0 << endl;
    
    // 方法2:用指针下标(不移动指针)
    ptr = scores;   // 重新指回开头
    total = 0;
    for (int i = 0; i < 5; i++) {
        total += ptr[i];   // 等效于 scores[i]
    }
    cout << "另一种算法总分:" << total << endl;
    
    return 0;
}

相关知识点指引

  • 指针与一维数组:这是最基础的应用,掌握后可以推广到二维数组(把二维数组看作一维数组的数组,用指针数组或行指针)。
  • 动态数组:用 new 在堆上分配内存,返回的也是指针,可以像操作数组一样用下标或指针移动。
  • 指针与字符串:字符串常量和字符数组本质上也是指针,char* 可以指向字符串。
  • 多级指针:比如 int** 指向一个指针数组,常用在动态二维数组或命令行参数中。
  • 函数指针:把函数的地址存入指针,可以灵活调用不同函数(比如排序时传比较函数)。

学完指针与数组,你就能理解为什么C++程序能又快又灵活地处理数据——因为指针给了你直接操作内存的能力,就像给了你一把万能钥匙!

例题精讲

1单选题

关于数组名和指针,以下说法正确的是? A. 数组名是一个指针变量,可以指向其他地址 B. 数组名是一个常量指针,其值不能被修改 C. 数组名和指向数组首元素的指针在使用上完全没有区别 D. 数组名实际上是一个数组类型的变量,具有数组大小信息

A数组名是一个指针变量,可以指向其他地址
B数组名是一个常量指针,其值不能被修改
C数组名和指向数组首元素的指针在使用上完全没有区别
D数组名实际上是一个数组类型的变量,具有数组大小信息
2判断题

若有定义:int a[5] = {1,2,3,4,5}, *p = a; 则表达式 *(p+3) 与 a[3] 的值相同。

3填空题
以下代码通过指针遍历数组并输出所有元素,请将空缺的表达式补充完整。

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
for (int i = 0; i < 5; ++i)
    cout << ___ << " ";
4单选题

对于二维数组 int a[3][4]; 以下哪个表达式表示 a[1][2] 的地址? A. *(a+1)+2 B. a[1]+2 C. a[1][2] D. *(*(a+1)+2)

A*(a+1)+2
Ba[1]+2
Ca[1][2]
D*(*(a+1)+2)
5填空题
以下函数通过指针参数交换两个整数,请补充指针的解引用操作。

void swap(int *x, int *y) {
    int temp;
    temp = *x;
    ___ = *y;
    *y = temp;
}