C++ 指针与数组

计算机会为每一个变量分配内存地址。在 C++ 中,数组是由多个同类型元素连续排列组成的数据结构,每一个元素在内存中都紧挨着依次存放。

正因为数组元素在内存中是连续排列的,数组和指针有着非常紧密的联系:在大多数表达式中使用数组名时,数组名都会自动转换为指向第 1 个元素的指针。

C++ 一维数组的指针

对于一维数组来说,数组名代表的就是第 1 个元素的内存地址,因此我们可以直接把数组名赋值给同类型的指针变量。

语法:

类型名* 指针变量名 = 数组名;

说明:

定义指向一维数组的指针与定义普通变量的指针语法相似,区别在于数组名前面不需要加取地址符 &。

一旦指针指向了数组首元素,我们既可以使用 *p 访问第 1 个元素,也可以直接在指针名后面加上中括号写成 p[i],像操作原数组一样按位置访问各个元素。

示例 1:使用指针访问一维数组

#include <iostream>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;

    std::cout << "arr = " << arr << '\n';
    std::cout << "p = " << p << '\n';
    std::cout << "*p = " << *p << '\n';

    for (int i = 0; i < 5; i++) {
        std::cout << p[i] << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

arr = 0x7ffdf3a37cc0
p = 0x7ffdf3a37cc0
*p = 10
10 20 30 40 50

分析:

int* p = arr; 表示把数组 arr 首元素的内存地址赋给整型指针 p,因此 arr 和 p 打印出来的地址完全相同,通过 *p 拿到的就是首元素的值 10。随后在循环中使用 p[i],就能依次取出数组里的全部 5 个元素。

为什么给普通变量取地址时要加 &,而这里的数组名前面不用加 & 呢?这是因为数组名 arr 本身就代表第 1 个元素 arr[0] 的地址,也就是说 arr 等价于 &arr[0]。对于这个例子来说,下面 2 种方式是等价的。

// 方式 1(推荐)
int* p = arr;

// 方式 2
int* p = &arr[0];

需要注意的是,数组的定义和指针的定义必须分开写,不能直接写成 int* p = {10, 20, 30}。因为花括号初值列表是一组具体的整数,并不是内存地址。

注意 p[i] 这种下标写法专门用于指向数组的指针。如果指针 p 只是指向单个普通变量,小伙伴们千万不要使用 p[1]、p[2] 越界访问未知内存,统一使用 *p 读写目标变量即可。

既然数组名能直接赋给指针,还能同样使用 [] 下标,那数组本身是不是就等同于指针呢?实际上并不是。我们可以使用 sizeof 运算符对比一下数组名与指针变量占用的字节数。

示例 2:使用 sizeof 对比数组与指针

#include <iostream>

int main() {
    int arr[4] = {10, 20, 30, 40};
    int* p = arr;

    std::cout << "sizeof(arr) = " << sizeof(arr) << '\n';
    std::cout << "sizeof(p) = " << sizeof(p) << '\n';
    return 0;
}

运行结果如下。

sizeof(arr) = 16
sizeof(p) = 8

分析:

从运行结果可以看出,sizeof(arr) 的结果是 16,而 sizeof(p) 的结果是 8。每个 int 元素占 4 个字节,数组 arr 包含 4 个元素,一共占用 4 × 4 = 16 个字节。而在 64 位系统下,指针变量 p 本身固定只占 8 个字节。

同样地,对于原数组 arr,我们可以直接调用 std::size(arr) 来获取数组长度。但对于指针 p,写 std::size(p) 会直接编译报错,因为原数组知道自己包含多少个元素,而指针 p 只记录了首元素从哪个地址开始。

C++ 指针的算术运算

当指针指向数组元素时,我们可以对指针进行算术运算,主要包括 3 种情况:① 指针加上或减去整数;② 指针自增或自减(++ / --);③ 指向同一个数组的两个指针相减。下面我们先来看看指针加减整数的具体用法。

示例 3:指针加减整数访问数组

#include <iostream>

int main() {
    int arr[4] = {10, 20, 30, 40};
    int* p = arr;

    for (int i = 0; i < 4; i++) {
        std::cout << (p + i) << " : " << *(p + i) << '\n';
    }
    return 0;
}

运行结果如下。

0x7ffdf3a37cc0 : 10
0x7ffdf3a37cc4 : 20
0x7ffdf3a37cc8 : 30
0x7ffdf3a37ccc : 40

分析:

(p + i) 表示从首元素地址 p 出发,向后偏移 i 个元素的位置。在它外层加上星号写成 *(p + i),就表示取出该位置上的元素值。

这里小伙伴们要注意:指针加 1 和普通整数加 1 是不一样的。普通整数 10 + 1 的结果是 11,而 int* 指针的每个目标元素占 4 个字节,所以 p + 1 代表地址往后移动 1 个 int 的步长(即 4 个字节)。观察输出地址的末尾变化(c0 → c4 → c8 → cc,十六进制中 c 代表 12),可以看到相邻两个元素的地址正好相差 4 个字节。

此外,我们在写 *(p + i) 时千万不能漏掉小括号。按照 C++ 的运算符优先级,解引用 * 的优先级高于加号 +,下面两种写法的意思完全不同。

int arr[4] = {10, 20, 30, 40};
int* p = arr;

std::cout << *(p + 1) << '\n';    // 输出 20
std::cout << *p + 1 << '\n';      // 输出 11

在上面代码中,*(p + 1) 是先让地址往后移 1 个元素,再取出 arr[1] 的值 20。而漏掉括号的 *p + 1 会先取出首元素 *p 的值 10,再加上 1 得到 11。

当我们通过指针访问一维数组的第 i 个元素时,主要有以下 2 种等价方式:

  • 下标法(推荐):写成 p[i](或 arr[i]),它是 *(p + i) 的简写形式,直观好懂,平时写代码最常用。
  • 指针法:写成 *(p + i)(或 *(arr + i)),先计算偏移后的内存地址,再通过 * 解引用访问元素。

无论是使用指针法 *(p + i) 还是下标法 p[i],我们不仅可以读取数组元素,也可以直接修改数组元素的值。

示例 4:通过指针修改数组元素

#include <iostream>

int main() {
    int arr[4] = {10, 20, 30, 40};
    int* p = arr;

    *(p + 1) = 25;    // 指针法修改元素
    p[2] = 35;        // 下标法修改元素

    for (int n : arr) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

10 25 35 40

分析:

在这个例子中,*(p + 1) = 25; 将下标为 1 的元素修改为 25,随后 p[2] = 35; 又将下标为 2 的元素修改为 35。因为指针 p 直接指向原数组 arr 所在的内存空间,修改完成后遍历原数组,里面的数据也就同步发生了改变。

除了使用 p + i 计算偏移地址之外,我们还可以对指针变量执行自增或自减运算(p++ / p--),或者把指向同一个数组的两个指针相减。

示例 5:指针自增与指针相减

#include <iostream>

int main() {
    int arr[4] = {10, 20, 30, 40};
    int* p = arr;          // 初始指向 arr[0]
    std::cout << *p << '\n';

    p++;                   // 后移指向 arr[1]
    std::cout << *p << '\n';

    int* end = &arr[3];    // 指向 arr[3]
    std::cout << "相差元素个数:" << (end - p) << '\n';
    return 0;
}

运行结果如下。

10
20
相差元素个数:2

分析:

指针 p 一开始指向首元素 arr[0],输出 10。执行 p++ 后,指针 p 自身保存的地址向后移动了 1 个元素的步长,改为指向 arr[1],因此再次输出 *p 得到 20。反之,执行 p-- 则会让指针向前退回上一个元素。

随后的 end - p 使用指向 arr[3] 的指针减去指向 arr[1] 的指针,算出的结果 2 就是两个指针之间相隔的元素个数(即下标 3 - 1 = 2)。

这里大家要区分清楚:指针变量 p 可以执行 p++ 改变指向,但数组名 arr 代表整个数组固定不变的起始地址。我们不能对数组名执行 arr++ 或重新赋值,否则会直接编译报错。

注意 我们使用指针偏移或自增访问数组时,一定要注意不能超出数组的边界。比如数组只有 4 个元素时,合法范围是 *(p + 0) 到 *(p + 3),一旦访问 *(p + 4) 就会造成数组越界,引发不可控的错误。

数组作为函数参数

当我们把一个数组作为函数参数传递时,为了省去复制整块数据的内存开销,C++ 并不会把整个数组复制一份传进去,而是只把 “数组第 1 个元素的地址” 传给函数。这种现象通常叫做 “数组退化为指针”。

也就是说,哪怕我们在函数形参里写成 int arr[],编译器在实际处理时也会自动把它当成指针 int* arr。下面 2 种函数声明方式是完全等价的。

// 方式 1
void doubleArray(int arr[], int size);

// 方式 2
void doubleArray(int* arr, int size);

既然函数内部拿到的 arr 本质上只是一个指向首元素的指针,就会带来以下 2 个直接影响:

  • 需要额外传入数组长度:在函数内部,形参 arr 已经退化成了指针,我们既不能调用 std::size(arr),使用 sizeof(arr) 测出的也只是指针本身的 8 个字节。因此我们需要在函数外部先算好元素个数,再作为参数一起传进去。
  • 修改形参会直接改变原数组:因为形参指针指向的就是外部原数组的内存空间,我们在函数内部修改 arr[i],函数外部的原数组也会同步改变。

示例 6:将数组作为参数传给函数

#include <iostream>
#include <iterator>     // 提供 std::size()

void doubleArray(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;    // 每个元素扩大为 2 倍
    }
}

int main() {
    int arr[4] = {10, 20, 30, 40};
    int size = std::size(arr);

    doubleArray(arr, size);

    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

20 40 60 80

分析:

在 main() 函数中,arr 仍然是完整的数组,引入 <iterator> 头文件后即可调用 std::size(arr) 算出元素个数 size 为 4。

调用 doubleArray(arr, size) 时,传入的 arr 自动转换为首元素的地址。函数内部执行 arr[i] *= 2 直接修改原数组的内存空间,因此回到 main() 函数打印原数组时,4 个元素全部变成了原来的 2 倍。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号