上篇聊了指针的本质,有人后台说想看指针和数组的关系。

来了。这个话题在面试里出现的频率,比你想象的高得多。

讲个我自己的翻车经历。

几年前面试一家做工业机器人的公司,技术面最后一轮,面试官是个干了十几年的老工程师。他问我:"数组名是常量指针吗?"

我想都没想,说:"是的。"

他笑了笑,追问:"那sizeof(arr)和sizeof(ptr)结果一样吗?"

我愣了一下,说:"不一样。"

他又问:"为什么?"

我支支吾吾说了半天,没说到点子上。最后他给我解释了,我才知道自己对指针和数组的理解有多浅。

今天就把这个知识点掰开了讲清楚。

指针运算:加减乘除,但不是你想的那样

先说指针运算。

指针可以加减整数,但加1不是地址加1,而是加一个"步长"——步长等于指针指向类型的大小。

int a = 10;
int* p = &a;
cout << p << endl;     // 比如0x7fff5a2b
cout << p + 1 << endl; // 不是0x7fff5a2c,而是0x7fff5a2f

为什么?因为int占4字节,p + 1是跳到下一个int的位置,地址加4。

这个特性在遍历数组的时候特别有用:

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
for (int i = 0; i < 5; i++) {
    cout << *(p + i) << " ";  // 输出1 2 3 4 5
}

*(p + i)等价于p[i]。这就是指针运算和数组下标的关系。

两个指针之间还可以做减法。如果两个指针指向同一个数组的元素,它们相减的结果是元素个数:

int arr[5] = {1, 2, 3, 4, 5};
int* p1 = &arr[1];
int* p2 = &arr[4];
cout << p2 - p1 << endl;  // 输出3

但注意,两个指针相加是没有意义的,编译器也不允许。指针运算只有加减,没有乘除。

指针之间还可以比较。如果两个指针指向同一个数组的元素,你可以用<、>、==来比较它们的大小——实际上就是在比较地址的高低:

int arr[5] = {1, 2, 3, 4, 5};
int* p1 = &arr[1];
int* p2 = &arr[4];
if (p1 < p2) {
    cout << "p1在前,p2在后" << endl;
}

这在写遍历逻辑的时候很常用。

数组名到底是不是指针?

回到开头那个面试问题。

"数组名是常量指针吗?"——这句话对了一半。

数组名在大多数表达式中会退化成指向首元素的指针。但数组名本身不是指针,它是一个常量地址。

区别在哪?看sizeof:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;

cout << sizeof(arr) << endl;  // 20(5个int,每个4字节)
cout << sizeof(ptr) << endl;  // 8(64位系统上指针占8字节)

sizeof(arr)是20,因为编译器知道arr是一个包含5个int的数组。sizeof(ptr)是8,因为ptr只是个指针。

如果数组名是指针,sizeof应该都是8。但结果不一样,说明数组名和指针在编译器眼里是两回事。

再看一个例子:

int arr[5] = {1, 2, 3, 4, 5};
arr++;  // 编译错误!

arr++编译报错,因为数组名是个常量地址,不能修改。但指针可以:

int* ptr = arr;
ptr++;  // 没问题,ptr现在指向arr[1]

所以准确的说法是:数组名在大多数情况下会退化成指针,但它本身不是指针变量。

面试中最爱考的三个坑

第一个坑:函数参数里的数组。

void printArray(int arr[]) {
    cout << sizeof(arr) << endl;  // 输出8,不是20!
}

很多人以为这里sizeof(arr)是20。错。函数参数里的int arr[]会被编译器当作int* arr,所以sizeof输出的是指针大小。

这就是为什么C++里函数参数要同时传数组和长度:

void printArray(int* arr, size_t size) {
    for (size_t i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
}

第二个坑:二维数组的指针运算。

int arr[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

arr是"指向数组的指针",类型是int(*)[4]。arr + 1跳过的是一整行(4个int,16字节),不是1个int。

arr[0]是第一行,类型是int*,arr[0] + 1跳过1个int(4字节)。

面试的时候,如果让你用指针访问二维数组的某个元素,别搞混了。

第三个坑:指针数组vs数组指针。

int* arr1[5];    // 指针数组:5个指针的数组
int (*arr2)[5];  // 数组指针:指向含5个int的数组的指针

arr1是数组,每个元素是int*。arr2是指针,指向一个int[5]。

读法:从变量名开始,先看[](数组),再看*(指针)。所以int* arr1[5]是"5个int指针的数组",int (*arr2)[5]是"指向int[5]数组的指针"。

在机器人开发里,这些知识有什么用?

你可能觉得这些都是课本知识,实际开发用不上。

错了。

在机器人开发里,传感器数据经常以数组形式存储。比如激光雷达一帧扫描数据,就是一个float数组:

float scan_data[360];  // 360个角度点的距离值

你要遍历这个数组、做滤波、做坐标变换,都要用到指针运算。

再比如点云数据,有时候为了性能,你会用一维数组存储三维点:

float points[1000 * 3];  // 1000个点,每个点3个坐标

访问第i个点的x坐标:points[i * 3]。访问y坐标:points[i * 3 + 1]。这就是指针运算的实际应用。

还有嵌入式开发里,硬件寄存器的映射经常用指针数组:

volatile uint32_t* gpio_regs[8];  // 8个GPIO端口的寄存器地址

搞清楚指针数组和数组指针的区别,在这种场景下就不会出错。

顺便说一句,现代C++里更推荐用std::array代替C风格的固定大小数组。std::array不会退化成指针,sizeof能正确返回整个数组的大小,还支持迭代器和STL算法。面试时如果你能主动提到std::array,说明你了解现代C++的最佳实践,不只是停留在课本层面。

面试的时候还有一种追问方式特别刁钻:面试官会问你&arr和arr有什么区别。表面上看它们地址值一样,但类型完全不同。arr退化后是int*,加1跳过一个int;&arr是int(*)[5],加1跳过整个数组。你写个小程序验证一下就会发现,arr + 1和&arr + 1的地址差了整整16个字节。这个知识点很多候选人答不上来,但如果你能讲清楚类型差异导致步长不同,面试官会眼前一亮。

另外实战中还有个容易忽略的点:指针越界。C++编译器不会帮你检查数组越界,*(p + 100)如果超出了数组范围,就是未定义行为。调试这类bug最好的办法是用AddressSanitizer,编译时加-fsanitize=address参数,运行时一旦越界立刻报错,比你自己肉眼排查高效十倍。

我见过一个实习生,把int* arr[5]写成了int (*arr)[5],结果程序跑飞了。查了半天bug,最后发现是指针类型搞错了。这种低级错误,面试的时候不会犯,实际开发里经常犯。

给正在准备面试的你一点建议

指针和数组的关系,面试考得细。

我建议你做三件事:

第一,搞清楚"数组名退化成指针"的含义,知道什么时候退化、什么时候不退化(sizeof和&两种情况不退化)。

第二,能手算指针运算的结果,特别是p + 1到底加了多少字节。

第三,分清指针数组和数组指针,知道二维数组的指针运算规则。

面试的时候,如果你能把这三个点讲清楚,面试官会觉得你的C++基础是扎实的。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列 上一篇:第11篇 指针的本质——内存地址的"门牌号"比喻 下一篇预告:第13篇 动态内存分配new/delete——堆内存管理的入门

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐