![int* p[4]和int(*p)[4]的区别:指针数组与数组指针详解](http://pic.xiahunao.cn/yaotu/int* p[4]和int(*p)[4]的区别:指针数组与数组指针详解)
int* p[4] 和 int(*p)[4] 的区别超详细如果你写过 C 或者 C肯定被这两个声明折磨过。int* p[4]和int(*p)[4]肉眼看去就差一对括号可两者表达的东西完全不同一个是装了 4 个指针的数组一个是只指向“一整块能装 4 个 int 的数组”的指针。当年我第一次在面试题里看到这俩的时候直接懵了后来靠右左法则加上画内存图才彻底搞定。今天就把我自己踩过的坑、总结出来的理解方法全部梳理一遍保证你看完能跟别人讲明白。这篇内容适合所有学 C/C 的读者不管是刚学指针的新手还是准备面试、刷八股的老手都能从这里拿到一套可复用的分析方法。我会先讲读声明的通用法则再分别拆解两种类型的内存结构、典型用法最后给一份常见错误排查清单。1. 先搞懂怎么读 C 的复杂声明1.1 运算符优先级决定了谁是主角要区分int* p[4]和int(*p)[4]本质上是看[]下标运算符和*解引用运算符谁先跟p结合。C 语言里[]的优先级高于*所以在int* p[4]中p先跟[4]结合说明p首先是一个数组数组有 4 个元素每个元素的类型是int*也就是整型指针。在int(*p)[4]中括号强制让*先跟p结合说明p首先是一个指针这个指针指向的东西是int[4]也就是一个包含 4 个int的数组。一句话先记住结论int* p[4]本质是“数组”int(*p)[4]本质是“指针”。前者叫指针数组后者叫数组指针。这个xx 数组数组 xx的叫法恰恰是新手最容易绕晕的地方——两个词的语序正好相反含义也完全相反。1.2 用右左法则三步读出任何声明我强烈推荐一个读复杂声明的通用套路叫右左法则Right-Left Rule。步骤很简单找到声明中的标识符变量名从它开始。先往右看遇到]或)就先读括号里的内容遇到*就记录是指针。再往左看直到把整个声明读完然后从内向外逐步翻译成自然语言。拿int* p[4]举例从p开始先往右看看到[4]说明p是数组长度 4。再往左看看到int*说明数组元素类型是int*。所以读作p是一个数组里面有 4 个元素每个元素都是指向int的指针。再看int(*p)[4]从p开始先往右看是一个)这表示括号没结束不能跨出去。往左看看到*说明p是指针。括号结束继续往右看看到[4]说明指针指向的对象是一个长度为 4 的数组。再往左看看到int说明数组中每个元素是int。读作p是一个指针它指向一个包含 4 个int的数组。注意右左法则的关键是先右后左、由内向外遇到括号必须处理完括号内的内容再往外扩展。多练几次以后看到char* argv[]、int (*fp)(int)这种声明都能一眼看懂。1.3 用typedef验证理解是否正确如果你对自己读声明的结果不放心可以借助typedef来验证。我会先把声明的读法转成typedef再用人话描述一遍// 声明一p 是数组元素是指针 typedef int* PtrArray[4]; PtrArray p; // 等价于 int* p[4] // 声明二p 是指针指向数组 typedef int Array4[4]; typedef Array4* ArrayPtr; ArrayPtr p; // 等价于 int(*p)[4]这里有个非常有意思的地方typedef int Array4[4];是先给int[4]这个数组类型起了个别名再定义指向这个数组的指针。写代码的时候如果觉得某个复杂声明太难记就先用typedef拆开一年后回头看也能秒懂。这也是很多开源项目里大量使用typedef的原因不是因为花哨而是真的能救命。2. 内存模型两者到底长什么样2.1int* p[4]的内存结构int* p[4]表示数组中有 4 个指针每个指针可以独立指向一个int或者一个int数组的开头。在内存里这 4 个指针是连续存放的每个指针在 64 位系统上占 8 字节所以整个数组占用 32 字节。但这 4 个指针指向的内存地址可以完全不连续它们各自指向哪里完全由你赋值决定。画个简单的图理解一下p[0] p[1] p[2] p[3] // 连续存放的 4 个指针 | | | | v v v v int int int int // 各自指向不同的 int 变量实际代码里指针数组最常见的用途是存储一组字符串指针或者存储一组动态分配的内存块。每个元素都是独立分配的互不干扰。2.2int(*p)[4]的内存结构int(*p)[4]表示p这个指针指向一整块长度为 4 的 int 数组。也就是说p自己只是一个指针变量占 8 字节64 位系统它指向的数组里存了 4 个int这 4 个 int 是连续排列的。int arr[4] {1, 2, 3, 4}; int (*p)[4] arr; // p 指向整个数组这里有一个新手特别容易犯的错把int (*p)[4] arr;直接赋值。arr是数组名它作为右值时会退化成指向首元素的指针类型是int*而p的类型是int(*)[4]两者类型不匹配编译器会报 warning 甚至 error。正确的写法是arr因为对数组名取地址得到的就是int(*)[4]类型。提示数组名arr在绝大多数表达式中会退化为int*但arr不退化为指针它保留了整个数组的信息得到的是数组指针。理解了这一点就理解了为什么int (*p)[4] arr;才是配对写法。2.3 用sizeof直接验证两者差异口说无凭写段代码验证一下最直接#include stdio.h int main(void) { int *p_arr[4]; // 指针数组4 个 int* int arr[4] {1, 2, 3, 4}; int (*arr_ptr)[4] arr; // 数组指针指向 int[4] printf(sizeof(p_arr) %zu\n, sizeof(p_arr)); // 4 * 8 32 printf(sizeof(p_arr[0]) %zu\n, sizeof(p_arr[0])); // 8一个指针 printf(sizeof(arr_ptr) %zu\n, sizeof(arr_ptr)); // 8一个指针 printf(sizeof(*arr_ptr) %zu\n, sizeof(*arr_ptr)); // 4 * 4 16 printf(sizeof((*arr_ptr)[0]) %zu\n, sizeof((*arr_ptr)[0])); // 4 return 0; }在 64 位系统上运行结果就是注释里写的p_arr整个数组占 32 字节arr_ptr本身只占 8 字节但它解引用后*arr_ptr是那个数组占 16 字节。这组sizeof是最直观的验货方式面试时如果能当场写出这个结果考官基本就心里有数了。3. 两种类型的实际用法拆解3.1 指针数组的典型场景指针数组最常见的应用是存储多个字符串也就是二维字符数组的替代方案。用char* str_arr[3]存储三个字符串常量比char str_arr[3][20]更节省空间因为每个字符串可以按实际长度存放前提是字符串常量区存储。#include stdio.h int main(void) { const char* str_arr[3]; str_arr[0] hello; str_arr[1] world; str_arr[2] c programming; for (int i 0; i 3; i) { printf(%s\n, str_arr[i]); } // 指针数组也是二维指针的基础 const char** pp str_arr; printf(%s\n, *(pp 1)); // world return 0; }再扩展一下指针数组里也可以存动态分配的内存块地址比如链表节点指针的集合、对象指针的集合。这种结构下数组本身是固定大小的容器容器里每个元素指向独立分配的资源释放的时候要逐个free不能只释放数组本身。3.2 数组指针的典型场景数组指针最典型的应用是操作二维数组。当你想写一个函数参数是一个二维数组的某一行或者整个二维数组时数组指针就派上用场了。#include stdio.h void print_row(int (*row)[4]) { for (int i 0; i 4; i) { printf(%d , (*row)[i]); } printf(\n); } void print_matrix(int (*matrix)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { printf(%d , matrix[i][j]); } printf(\n); } } int main(void) { int mat[3][4] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; // 传入单行取第 1 行的地址 print_row(mat[1]); // 传入整个二维数组 print_matrix(mat, 3); return 0; }注意一个细节print_matrix的参数int (*matrix)[4]和传入的mat类型是匹配的因为mat退化后正是指向第一行的指针也就是int(*)[4]类型。而print_row(mat[1])取的是第二行的地址类型同样是int(*)[4]。这就是数组指针和二维数组的天然配合。3.3 两者混用的经典错误很多人在写代码时会不自觉地把这两种类型混用导致编译出错或者运行崩溃。我列几个高频错误// 错误 1把指针数组当成数组指针用 int* p[4]; int arr[4] {1, 2, 3, 4}; p arr; // 编译错误p 是数组名不能赋值 // 错误 2把数组指针当成指针数组用 int (*q)[4]; q[0] arr; // 编译错误q[0] 是 int[4]不是 int*错误 1 里p是数组名数组名不能作为左值被赋值这是语法层面的硬性错误。错误 2 里q[0]的类型是int[4]你拿int*arr退化后的类型去赋值类型不兼容。真正正确的写法是要么分别初始化各自数组的元素要么用数组指针指向已存在的二维数组int* p[4]; int a 1, b 2, c 3, d 4; p[0] a; p[1] b; p[2] c; p[3] d; int mat[2][4] {{1,2,3,4},{5,6,7,8}}; int (*q)[4] mat; // 正确q 指向 mat 的第一行4. 初始化、赋值和运算的完整对比4.1 二者的初始化方式对比表把两种类型放在一张表里对比记忆点会更清晰操作int* p[4]指针数组int(*p)[4]数组指针本质数组长度 4元素是指针指针变量指向 int[4]自身大小4 * 8 32 字节8 字节64 位系统初始化方式p[0] a;逐个赋值p arr;或p matrix;访问单个 int*(p[i])或*p[i](*p)[i]访问第 i 行第 j 列*(p[i] j)p[i][j]当 p 指向二维数组典型场景字符串数组、指针集合二维数组的行操作、函数参数这张表最核心的是访问单个 int那一行。*p[i]等价于*(p[i])因为[]优先级高于*所以它的意思是先取p[i]一个指针再解引用。而(*p)[i]是先对p解引用得到数组再去下标。括号的位置直接决定了运算顺序。4.2 访问二维数组时的行为差异假设你有一个二维数组int matrix[2][4]想用两种方式分别访问它观察到的行为完全不同int matrix[2][4] { {1, 2, 3, 4}, {5, 6, 7, 8} }; // 方式一用指针数组模拟 int* ptr_arr[2]; ptr_arr[0] matrix[0]; // 指向第一行 ptr_arr[1] matrix[1]; // 指向第二行 printf(%d\n, ptr_arr[1][2]); // 7因为 ptr_arr[1] 是 int*可以继续下标访问 // 方式二用数组指针指向二维数组 int (*arr_ptr)[4] matrix; printf(%d\n, arr_ptr[1][2]); // 7arr_ptr[1] 实际上指向第二行看到没有最终结果一样但内部逻辑完全不同ptr_arr[1][2]是先取ptr_arr[1]这个指针然后把它当成int*加 2 再解引用arr_ptr[1][2]是先算arr_ptr 1此时指针移动的步长是 4 个 int得到指向第二行的数组指针再取第二行的第 2 个元素。arr_ptr 1能正确跳到下一行正是因为它知道自己指向的数组长度是 4 个 int所以指针算术以整个数组为单位移动。这就是数组指针最核心的机制指针类型里的[4]规定了指针加减的步长。如果你把int(*p)[4]改成int(*p)[5]那p 1就会跳过 5 个 int。步长完全由声明中的数组维度决定这个细节在实际项目中经常是 bug 的根源。4.3 函数参数中的退化陷阱我刚学 C 的时候总以为数组传参时会完整传进去后来才意识到数组参数会退化成指针。这个机制跟今天的话题关系极大int* p[4]作为函数参数时会退化成int**因为数组名等价于指向首元素的指针首元素是int*所以就是指向指针的指针。int(*p)[4]本身是指针作为参数不会进一步退化它照常以指向数组的指针形式传入。// 等价写法 void func1(int* p[4]); // 等价于 void func1(int** p); void func2(int (*p)[4]); // 仍然是 int(*)[4] // 调用 int* arr[4]; func1(arr); // 传指针数组类型退化为 int** int mat[3][4]; func2(mat); // mat 退化为 int(*)[4]即指向第一行的数组指针这一点在刷 LeetCode 或者读底层代码时经常会碰到。比如很多 C 项目里写矩阵操作的函数参数通常就是int (*a)[N]这种形式因为这样才能在函数内部正确计算二维下标。而如果你看到有人写void process(int** a)接收二维数组那多半只能传指针数组动态分配的结构不能直接传静态二维数组。两种二维方案是有本质区别的。5. 常见问题与排查技巧实录5.1 编译报错速查表我在实际开发和带新人的过程中整理出一份高频错误对照表可以说覆盖了 90% 的踩坑场景错误现象可能原因解决办法cannot convert int* to int (*)[4]用arr而不是arr赋值给数组指针改成arr或直接让数组指针指向二维数组名lvalue required as left operand of assignment试图给数组名p赋值数组名不是左值只能给数组的元素赋值subscripted value is neither array nor pointer对int(*p)[4]用p[0][0]却忘记 p 未初始化先让 p 指向一个有效数组再访问函数内sizeof(p)结果不对在形参里用了sizeof形参已退化为指针同时传入数组长度或改用模板/结构体包装释放内存时崩溃指针数组里存了指向栈变量的地址却调用 free检查每个指针的指向来源只释放动态分配的内存5.2 一个我调试过的真实案例有一次我同事写了一个字符串排序的函数参数声明是char* arr[]函数内部用sizeof(arr) / sizeof(arr[0])来求元素个数。结果在函数内计算出的长度一直是 164 位机器上指针大小 8 除以指针大小 8导致排序逻辑完全乱掉。这就是典型的数组参数退化问题char* arr[]看似是数组实际上编译器把它当作char**处理sizeof只能拿到指针的大小拿不到数组长度。解决办法是在外部算好长度再传进去void sort_strings(char* arr[], int n) { // 排序逻辑用 n 作为数组长度 } int main(void) { char* str_arr[] {banana, apple, cherry}; int n (int)(sizeof(str_arr) / sizeof(str_arr[0])); sort_strings(str_arr, n); return 0; }这个问题的根子就是对指针数组作为参数会退化理解不到位。你要是今天把int* p[4]和int(*p)[4]的区别搞清楚了这种坑基本能绕开一大半。5.3 调试时的实用技巧排查这类指针问题我自己的经验就三条第一条打印sizeof和指针地址别猜。我发现很多人调试指针问题靠瞪眼其实打印几行信息比啥都快。把sizeof(p)、sizeof(*p)、p的值、p的值全部打出来类型和内存布局立刻清晰。第二条用typedef把复杂声明拆开降低阅读成本。比如typedef int (*Array4Ptr)[4];之后函数参数写Array4Ptr p比写int (*p)[4]好懂得多尤其是项目代码里层层嵌套的时候。第三条开启编译器的-Wall -Wextra警告项。很多类型不匹配的问题编译器会给出非常明确的提示不要忽略 warning特别是类型转换相关的 warning几乎每一个都值得认真看一遍。5.4 面试和笔试中的高频考点这类题是 C 语言面试的常客因为一个声明就能看出候选人对运算符优先级、指针、数组三个知识点的掌握程度。常见问法有说出int* p[4]的类型是什么。说出int(*p)[4]的类型是什么。p 1在这两种声明下分别移动多少个字节。如何用数组指针实现一个可以打印任意行数、固定列数的二维数组的函数。sizeof(p)、sizeof(*p)的值各是多少。回答的时候建议按我上面的思路来先讲运算顺序[]优先还是*优先再讲内存布局最后给一个实际使用场景。这套回答逻辑本身就展示了你的理解深度比背答案效果好得多。我个人在实际教学和写代码中最大的体会是这两个声明之所以容易混淆就是因为初学者容易死记指针数组数组指针这六个字而没有真正建立运算符优先级决定结合顺序结合顺序决定类型类型决定内存布局这条思维链。你只要把这条链打通了以后遇到int (*fp)(int)、char* (*p)[8]这种更复杂的声明都能用同一套方法拆解再也不用靠死记硬背。最后再分享一个小技巧拿到任何复杂声明先在纸上画出它的层级树把数组、指针、基础类型分别标出来再对着层级树写代码。花两分钟画图能省下两小时调试时间这个习惯我一直用到现在。