在 C++ 中,int* p[3] 和 int (*p)[3] 看起来只差了一对小括号,但代表的意思却完全不同:前者叫做 “指针数组”,后者叫做 “数组指针”。
区分这两个概念时,我们只需要在名词中间加上一个 “的” 字,看最后落脚在哪个词上,就能一眼认清它们的本质:
- 指针数组:即 “指针的数组”(如
int* p[3]),落脚点在 “数组”,说明它本质上是一个数组,只不过数组里的每一个元素都是指针。 - 数组指针:即 “数组的指针”(如
int (*p)[3]),落脚点在 “指针”,说明它本质上是一个指针变量,只不过它指向了一整个数组。
C++ 指针数组
指针数组首先是一个 “数组”。平时我们定义的普通整型数组(如 int arr[3])里面装的是一个个整数,而指针数组里面装的则是一个个内存地址。
1. 指针数组的语法
定义指针数组时,我们只需要把元素类型写成指针类型(如 int*、const char*),后面跟上数组名和长度即可。
语法:
类型名* 数组名[长度];说明:
按照 C++ 的运算符优先级,中括号 [] 的优先级高于星号 *。因此在 int* pArr[3] 中,变量名 pArr 会先与右边的 [3] 结合,表明 pArr 是一个包含 3 个元素的数组。接着再与左边的 int* 结合,表明数组里每个元素的类型都是 int* 指针。
示例 1:定义并使用指针数组
#include <iostream>
int main() {
int a = 10;
int b = 20;
int c = 30;
int* pArr[3] = {&a, &b, &c}; // 存放 3 个指针的数组
for (int i = 0; i < 3; i++) {
std::cout << pArr[i] << " : " << *pArr[i] << '\n';
}
return 0;
}运行结果如下。
0x7ffdf3a37cc4 : 10
0x7ffdf3a37cc0 : 20
0x7ffdf3a37cbc : 30分析:
int* pArr[3] = {&a, &b, &c}; 定义了一个名为 pArr 的指针数组,里面的 3 个元素分别保存了变量 a、b、c 的内存地址。由于 pArr[i] 取出来的是一个指针,在它前面加上星号写成 *pArr[i],就能解引用拿到对应的整数值。
2. 保存多个 C 风格字符串
在传统的 C/C++ 代码中,指针数组最常见的用途是用来保存多个长度不一的 C 风格字符串。我们先来看看如果使用二维字符数组保存多个字符串,会遇到什么麻烦。
示例 2:二维字符数组保存字符串
#include <iostream>
int main() {
char country[3][16] = {
"China",
"Japan",
"Australia"
};
for (int i = 0; i < 3; i++) {
std::cout << country[i] << '\n';
}
return 0;
}运行结果如下。
China
Japan
Australia分析:
在这个例子中,char country[3][16] 为了装下最长的字符串,必须规定每一行的固定长度都是 16 个字节。但像 "China" 和 "Japan" 实际只有 5 个字母,算上结尾的 \0 也只占 6 个字节,每一行剩下的 10 个字节都被白白浪费了。
为了省去这种按最长文本对齐列宽的空间开销,我们就可以改用字符指针数组来实现。
示例 3:指针数组保存多个字符串
#include <iostream>
int main() {
const char* country[3] = {
"China",
"Japan",
"Australia"
};
for (int i = 0; i < 3; i++) {
std::cout << country[i] << '\n';
}
return 0;
}运行结果如下。
China
Japan
Australia分析:
const char* country[3] 定义了一个包含 3 个元素的指针数组。数组里的每个元素都是一个 const char* 指针,只负责记录各个字符串首字符的地址。这样每个字符串实际有多长就占多少内存,不再需要按固定列宽补齐空白字节。
初始化指针数组时,我们同样可以把中括号里的长度省略掉,交由编译器根据初值个数自动推算。下面 2 种方式是等价的。
// 方式 1
const char* country[3] = {"China", "Japan", "Australia"};
// 方式 2
const char* country[] = {"China", "Japan", "Australia"};当然在现代 C++ 日常开发中,如果不涉及底层接口兼容,小伙伴们直接使用 std::vector<std::string> 管理多个字符串会更加省心。
注意 像 "China" 这样的字符串字面量存放在只读内存区,类型为 const char[]。我们在定义指向它的指针数组时必须加上 const 写成 const char*,不能写成 char*。
C++ 数组指针
数组指针跟指针数组正好相反,它本质上是一个单独的 “指针变量”,指向一整块数组。下面我们就来看看指向一维数组和指向二维数组的数组指针具体怎么使用。
1. 指向一维数组的数组指针
定义数组指针时,我们必须使用小括号把星号和指针名括起来,写成 (*指针变量名)。
语法:
类型名 (*指针变量名)[长度];说明:
因为小括号 () 的优先级最高,所以在 int (*p)[3] 中,我们先看括号内部的 *p 确定 p 是一个指针,再看外面的 int [3] 确定它指向包含 3 个 int 元素的数组。这里的小括号一定不能省略,一旦去掉小括号写成 int* p[3],就会变成前面讲的指针数组。
示例 4:指向一维数组的数组指针
#include <iostream>
int main() {
int arr[3] = {10, 20, 30};
int (*p)[3] = &arr; // 指向整个一维数组 arr
std::cout << "arr = " << arr << '\n';
std::cout << "arr + 1 = " << (arr + 1) << '\n';
std::cout << "p = " << p << '\n';
std::cout << "p + 1 = " << (p + 1) << '\n';
for (int i = 0; i < 3; i++) {
std::cout << (*p)[i] << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
arr = 0x7ffdf3a37cc0
arr + 1 = 0x7ffdf3a37cc4
p = 0x7ffdf3a37cc0
p + 1 = 0x7ffdf3a37ccc
10 20 30分析:
int (*p)[3] = &arr; 使用 &arr 取出整个一维数组 arr 的地址,并赋给数组指针 p。可能有些小伙伴会问:“arr 和 &arr 打印出来的起始地址都是 0x7ffdf3a37cc0,它们到底有什么区别呢?”
对比 arr + 1 和 p + 1 的输出地址就能看清本质。普通数组名 arr 代表首元素 arr[0] 的地址(类型为 int*),因此 arr + 1 只往后跨 1 个 int 元素,地址从 cc0 变为 cc4,增加了 4 个字节。
而 &arr 代表包含 3 个元素的整块数组的地址(类型为 int (*)[3])。因此 p + 1 会直接跨过整块数组,地址从 cc0 变为 ccc(十六进制中 c 代表 12),一口气增加了 3 × 4 = 12 个字节。
正因为 p 指向的是整块一维数组,我们不能直接写 p[1] 去取元素,否则指针会直接跨出整块数组造成越界。我们需要先写 (*p) 解引用拿到数组 arr 本身,再加上下标写成 (*p)[i],才能准确取出第 i 个元素。
2. 指向二维数组的数组指针
操作普通的一维数组时,我们直接使用 int* p = arr 就足够了。在实际开发中,数组指针最常见的应用场景是用来指向和操作二维数组。
对于二维数组 int arr[2][3],如果我们直接写 int* p = arr,编译器会直接报错。这是因为 2 行 3 列的 arr[2][3] 可以看作由 2 个 “长度为 3 的一维数组” 组成,它的第 0 个元素 arr[0] 并不是单个整数,而是第 0 整行。因此二维数组名 arr 代表的就是第 0 整行的地址 &arr[0],类型正好是数组指针 int (*)[3]。
语法:
类型名 (*指针变量名)[列数] = 二维数组名;说明:
中括号里的数字必须是二维数组每一行包含的元素个数(即列数),而不是行数。此外,二维数组名 arr 本身就已经代表第 0 行的地址,前面不需要再加 &。
示例 5:使用数组指针访问二维数组
#include <iostream>
int main() {
int arr[2][3] = {
{10, 20, 30},
{40, 50, 60}
};
int (*p)[3] = arr; // 指向二维数组的第 0 行
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << p[i][j] << ' ';
}
std::cout << '\n';
}
return 0;
}运行结果如下。
10 20 30
40 50 60分析:
在这个例子中,二维数组 arr 有 2 行 3 列,每一行包含 3 个 int 元素,因此我们使用 int (*p)[3] = arr 定义指向它的数组指针。因为二维数组名 arr 等价于第 0 行的地址 &arr[0],所以下面 2 种初始化方式是完全等价的。
// 方式 1(推荐)
int (*p)[3] = arr;
// 方式 2
int (*p)[3] = &arr[0];为什么示例 4 操作一维数组时不能直接写 p[i],而这里操作二维数组却能直接写 p[i][j] 呢?因为示例 4 只有一行数据,p + 1 会跨出数组边界。而这里的二维数组有多行数据,p 指向第 0 行,我们可以按 “先选行、再选列” 分两步拆开来看:
- 先选中第 i 行:
p + i会整行往后移动,指向第i行。解引用后的*(p + i)(即p[i])拿到的就是第i整行的一维数组,它在表达式中又代表该行首元素的地址。 - 再取出第 j 列:在
*(p + i)的基础上加上j偏移到第j列,最外层再解引用写成*(*(p + i) + j),就取出了第i行、第j列的元素值。
平时写代码时,大家不需要手写 *(*(p + i) + j) 这种多层星号嵌套,直接使用等价的 p[i][j] 下标写法即可,既直观又不容易出错。
理解了这一点,小伙伴们也就明白为什么把二维数组作为函数参数传递时,第 1 个中括号的行数可以省略,但第 2 个中括号的列数一定不能省略了。下面 2 种函数形参写法在编译器眼里是完全等价的。
// 方式 1
void printMatrix(int arr[][3], int rows);
// 方式 2
void printMatrix(int (*arr)[3], int rows);也就是说,二维数组传入函数时,底层退化成的正是数组指针 int (*arr)[3]。编译器必须明确知道每一行包含多少个元素,才能准确算出 arr + 1 跨过一整行需要移动多少个字节。
