计算机会为每一个变量分配内存地址。在 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 倍。
