
上篇聊了指针的本质有人后台说想看指针和数组的关系。来了。这个话题在面试里出现的频率比你想象的高得多。讲个我自己的翻车经历。几年前面试一家做工业机器人的公司技术面最后一轮面试官是个干了十几年的老工程师。他问我数组名是常量指针吗我想都没想说是的。他笑了笑追问那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; }这在写遍历逻辑的时候很常用。数组名到底是不是指针回到开头那个面试问题。数组名是常量指针吗——这句话对了一半。数组名在大多数表达式中会退化成指向首元素的指针。但数组名本身不是指针它是一个常量地址。区别在哪看sizeofint arr[5] {1, 2, 3, 4, 5}; int* ptr arr; cout sizeof(arr) endl; // 205个int每个4字节 cout sizeof(ptr) endl; // 864位系统上指针占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个int16字节不是1个int。arr[0]是第一行类型是int*arr[0] 1跳过1个int4字节。面试的时候如果让你用指针访问二维数组的某个元素别搞混了。第三个坑指针数组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跳过一个intarr是int(*)[5]加1跳过整个数组。你写个小程序验证一下就会发现arr 1和arr 1的地址差了整整16个字节。这个知识点很多候选人答不上来但如果你能讲清楚类型差异导致步长不同面试官会眼前一亮。另外实战中还有个容易忽略的点指针越界。C编译器不会帮你检查数组越界*(p 100)如果超出了数组范围就是未定义行为。调试这类bug最好的办法是用AddressSanitizer编译时加-fsanitizeaddress参数运行时一旦越界立刻报错比你自己肉眼排查高效十倍。我见过一个实习生把int* arr[5]写成了int (*arr)[5]结果程序跑飞了。查了半天bug最后发现是指针类型搞错了。这种低级错误面试的时候不会犯实际开发里经常犯。给正在准备面试的你一点建议指针和数组的关系面试考得细。我建议你做三件事第一搞清楚数组名退化成指针的含义知道什么时候退化、什么时候不退化sizeof和两种情况不退化。第二能手算指针运算的结果特别是p 1到底加了多少字节。第三分清指针数组和数组指针知道二维数组的指针运算规则。面试的时候如果你能把这三个点讲清楚面试官会觉得你的C基础是扎实的。如果这篇文章对你有帮助欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。「机器人软件开发面试·从入门到精通」连载系列上一篇第11篇 指针的本质——内存地址的门牌号比喻 下一篇预告第13篇 动态内存分配new/delete——堆内存管理的入门有任何问题欢迎评论区留言我会尽量回复。