C语言数组名与指针的区别:sizeof、a与函数传参陷阱 数组名和指针不是一回事这是 C 语言笔试里最高频的“送命题”也是很多人在实际代码里莫名其妙踩坑的重灾区。不少初学者背结论背得很熟数组名会退化成指针、a 和 a 一样、函数传数组传的是指针……但真到做题或调试时还是分不清sizeof(a)、a、a1到底等于什么。这篇文章就把这个坑从头拆一遍结合笔试常考题型和实际调试经验告诉你什么时候数组名是指针什么时候不是以及遇到这类题应该按什么顺序判断。如果你是准备校招笔试、刚学指针或者写 C 语言项目时被数组传参搞烦过这篇都值得看完。我不打算只列结论而是把判断思路和验证方式一起讲清楚让你下次看到int a[5]时能直接得出正确答案。1. 先把结论摆出来数组名到底是不是指针很多人学 C 语言时会听到一句话“数组名就是指针。”这句话在大方向上学着用没问题但严格抠起来是错的。数组名在大多数表达式里会“退化”成指向首元素的指针这是 C 语言标准里规定的数组到指针的转换规则。但数组名本身作为一个类型是int[5]这样的数组类型不是int*类型。1.1 数组名的本质看这段代码int a[5] {1, 2, 3, 4, 5}; printf(a 的类型推断%zu\n, sizeof(a)); printf(a0 的类型推断%zu\n, sizeof(a0));在 64 位环境里sizeof(a)结果是205 个 int每个 4 字节而sizeof(a0)结果是8一个指针的大小。如果数组名真是指针sizeof(a)也应该是 8但实际不是。原因在于a是数组对象本身sizeof作用于数组名时返回的是整个数组占用的字节数。而a0这个表达式里a先退化成了指向首元素的指针再进行加法运算结果类型是int*所以sizeof得到的是指针大小。判断一个标识符是不是指针最直接的方式就是看它的类型。a的类型是int[5]p如果声明成int *p类型才是int*。类型不同能做操作自然不同。1.2 什么时候数组名会“看起来像指针”数组名在以下情况中会退化成指向首元素的指针作为赋值操作的右值参与算术运算比如a1作为函数实参传递与指针比较或运算但以下情况不会退化sizeof(a)a取整个数组的地址对数组名使用typeof、_Alignof等编译期操作笔试经常挖的坑就是在这两类情况之间来回切换。比如int a[5]; printf(%p %p\n, a, a);打印出来的地址值通常是相同的同一个内存起始位置但类型不同a的类型是int*退化为首元素指针a的类型是int(*)[5]也就是指向整个数组的指针。地址值一样不代表类型一样后续做加减时结果会完全不同。2. 笔试最爱挖的坑sizeof、a、指针加减2.1 sizeof(a) 和 sizeof(p) 为什么不一样这是最直接的一个坑。看下面代码写出输出int a[5] {0}; int *p a; printf(%zu %zu\n, sizeof(a), sizeof(p));答案在 64 位环境是20 8。sizeof(a)是整个数组大小sizeof(p)是指针变量大小。如果数组作为函数参数传入情况又变了void func(int arr[]) { printf(%zu\n, sizeof(arr)); }这里sizeof(arr)是指针大小因为函数形参的数组写法会被编译器处理成指针。也就是说int arr[]和int *arr在这个函数里等价。这个坑特别容易让新手产生混乱为什么同一个名字在函数外是 20在函数内是 8。原因就是形参发生了“退化”。C 语言里数组作为参数传递时不会把整个数组拷贝进函数而是传递指向首元素的指针。这也是为什么函数里想拿到数组长度必须额外传一个长度参数。2.2 a 和 a[0] 的区别a[0]是取数组第一个元素的地址类型是int*。a是取整个数组的地址类型是int(*)[5]。两者的地址值一样但步长不一样。int a[5] {0}; printf(%p\n, a); // 首元素地址 printf(%p\n, a[0]); // 首元素地址 printf(%p\n, a); // 整个数组地址数值上和前面相同 printf(%p\n, a 1); // 地址 4 字节 printf(%p\n, a 1); // 地址 20 字节如果你把a 1强转成int*它指向的是整个数组之后的内存位置。很多笔试代码会写int a[5] {1,2,3,4,5}; int *p (int*)(a 1); printf(%d\n, p[-1]);结果是5。因为a1越过了整个数组再往回退一个 int刚好回到最后一个元素。这种题考的就是指针类型步长。做错了通常不是不会加减而是没有意识到a的类型决定1跳了多远。2.3 指针加减和数组下标数组下标a[i]本质上等价于*(a i)。但要注意这个等价性建立在a是数组名或指针的前提下。常见笔试题int a[5] {10, 20, 30, 40, 50}; int *p a; printf(%d %d\n, p[2], *(p 3));这题答案是30 40只要清楚下标就是偏移就行。但如果写成int *p a 4; printf(%d\n, p[-2]);答案是30。p指向最后一个元素p[-2]就是往前偏移两个元素指向a[2]。负数下标在 C 语言里是合法的指针偏移只是容易越界不是语法错误这是很多人没意识到的。再看出题人爱考的组合int a[3][4] {0}; printf(%zu\n, sizeof(a)); // 48 printf(%zu\n, sizeof(a[0])); // 16 printf(%zu\n, sizeof(a[0][0])); // 4如果对二维数组的“数组的数组”理解不深这里很容易写成sizeof(a)是 4 或 8。实际上a是包含 3 个int[4]的数组所以总大小是 3×4×448 字节。3. 函数传参时数组名做了什么函数传参是笔试和实际项目里最容易踩坑的地方。很多人写完一个数组处理函数发现sizeof(arr)不对或者二维数组传参报错都是因为没搞清楚函数形参里数组名发生了什么。3.1 形参退化成指针C 语言规定数组作为函数参数时会退化为指向首元素的指针。这个退化发生在编译期函数内部无法通过形参得到原始数组的大小。void print_array(int arr[], int n) { for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); }arr[]在编译器眼里就是int *arr。所以void print_size(int arr[10]) { printf(%zu\n, sizeof(arr)); // 8不是 40 }[10]只是给阅读者看的提示编译器不检查实参数组的实际长度。就算你传入int arr[5]编译器也不会报错因为形参就是指针。这带来的常见问题是很多初学者在函数里用sizeof(arr)/sizeof(arr[0])计算长度结果完全错误。正确做法是额外传入长度参数或者使用宏定义固定长度。3.2 二维数组传参的尺寸问题二维数组传参比一维更复杂。看这个函数void func(int arr[][4]) { // 可以访问 arr[i][j] }这里第二维必须写清楚因为编译器需要知道每一行有多少个元素才能计算arr[i]的偏移。如果写成int arr[][]编译会报错因为不知道每一行的长度无法确定行指针步长。也有人会这么写void func(int (*arr)[4]) { // 等价于 int arr[][4] }int (*arr)[4]是指向一行包含 4 个 int 的数组的指针。这种写法在函数内部可以用arr[i][j]和二维数组名退化的结果一致。请注意int *arr[]和int (*arr)[4]完全不同。前者是指针数组数组里每个元素都是int*后者是数组指针指向一个数组。笔试经常让判断这两个声明int *a[4]因为优先级[]高于*所以 a 先和[]结合是大小为 4 的指针数组而int (*a)[4]中 a 先和*结合是指向数组的指针。3.3 怎么设计函数签名才不踩坑实际项目里我更推荐用显式指针加长度参数而不是依赖数组形参的简化写法。原因有三个明确表达函数不修改数组时用const修饰指向的内容可读性好。长度单独传参避免函数内部调用sizeof。二维数组传参时明确写出列宽或用一维方式手动计算偏移不容易出边界问题。示例void process_matrix(int rows, int cols, int matrix[][cols]) { for (int i 0; i rows; i) { for (int j 0; j cols; j) { matrix[i][j] 1; } } }C99 支持变长数组VLA作为形参cols必须是函数参数中在前面出现的变量。编译时用支持 C99 或更高标准的编译器即可。在嵌入式环境里如果编译器对 VLA 支持不好就退回到int (*matrix)[COLS]把列宽定义为宏。4. 字符串、指针数组、多维数组的边界字符串和字符数组这一块笔试题非常爱考因为字符串字面量、字符数组、字符指针三者的内存位置和可修改性不同。很多人直接混淆导致代码运行后崩溃还不知道为什么。4.1 字符串字面量和字符数组看两段代码char s1[] hello; char *s2 hello;s1是一个字符数组内容存储在栈上或全局区取决于声明位置可以修改s1[0]。s2是一个指针指向字符串字面量。字符串字面量通常存储在只读区修改s2[0]会触发未定义行为很多系统上直接段错误。笔试里常写成char *p hello; p[0] H; // 危险操作正确答案是这是未定义行为程序可能崩溃。不应该修改字符串字面量指向的内容。但如果是char p[] hello; p[0] H; // 合法p 是可修改的数组这里 p 是数组名不是指针。虽然它经常被用来和字符串交互但类型是char[6]而不是char*。sizeof(p)是 6包含结尾空字符sizeof(s2)是 8指针大小这两者很容易被忽略。4.2 指针数组存放字符串指针数组适合存放多个字符串尤其是字符串长度不一致的情况。比如const char *names[] {Alice, Bob, Charlie};names是数组数组的每个元素是const char*。names[0]是一个指针指向字符串字面量Alice。如果用二维字符数组char names[][8] {Alice, Bob, Charlie};每个字符串都占 8 字节不足的部分补零。sizeof(names)是 24sizeof(names[0])是 8。这种方法适合字符串长度固定或较短的情况但浪费空间指针数组只存指针空间占用更小但字符串字面量本身仍然占存储。两者的另一个区别是二维数组的内容可以安全修改因为每一个char元素都是数组的一部分是可修改对象指针数组如果用const char*指向字面量则不能通过该指针修改内容。面试官经常用这个点来区分考生到底理解的是内存布局还是只记住了“字符串就是字符指针”。4.3 多维数组和指针数组区别二维数组和指针数组在访问形式上都是a[i][j]但底层逻辑完全不同。二维数组int a[3][4]的内存是连续分布的一整块 12 个 int。a[i][j]的地址计算公式是(int*)a i * 4 j。因为每一行长度固定所以编译器可以自动算偏移。指针数组int *p[3]中p本身占据 3 个指针的空间每个p[i]可以指向不同的内存区域。p[i][j]实际上是先取p[i]的值一个指针再对这个指针做偏移。三块内存不一定连续长度也可以完全不同。这也解释了为什么二维数组作函数参数时必须写列宽。如果只传int a[][]编译器不知道每行多长无法计算行偏移。而指针数组作参数直接写int *p[]或int **p都行因为每个元素本身就是一个指针不需要知道指向的数组有多长。实际开发中我的建议是如果数据是一张矩形的表用二维数组如果每一行长度差异很大或者要动态分配用指针数组或二级指针。5. 笔试现场怎么快速判断一张表和一套排查顺序笔试时间紧不能每道题都从头推一遍你要有快速判断的套路。我总结了一套顺序做题时按这个来准确率会高很多。5.1 常见判断表遇到一个表达式时先判断它是数组名还是指针变量再看操作符。这张表可以帮你快速定位表达式类型值/结果典型陷阱aint[5]表达式中退化为int*首元素地址不要误以为数组名永远是数组类型aint(*)[5]整个数组地址数值与首地址相同地址值相同但1步长不是 4是 5×sizeof(int)a[0]int第一个元素不是指针a[0]int*首元素地址和a数值相同类型也相同a 1int*跳过一个元素不要和a 1混淆a 1int(*)[5]越过整个数组如果强转成int*再减 1 指向最后一个元素sizeof(a)无整个数组字节数数组名没退化sizeof(a)无指针大小a是指针不是数组函数形参int arr[]int*指针用sizeof是错的int *p[5]指针数组数组元素是指针优先级[]高于*int (*p)[5]数组指针指向数组的指针不是数组是一个指针5.2 做题时的思考顺序遇到数组和指针题我会按以下流程判断先看这个操作符是什么sizeof、、*、[]、、-。如果是sizeof和操作数直接作用于数组名数组名不退化按数组类型处理。如果是一般运算表达式数组名退化为首元素指针按指针规则处理。看指针的类型而不是仅看地址值。类型决定步长。看是否涉及函数传参函数形参里的数组声明一律当作指针。再看是否涉及字符串字面量判断内存是否可修改。比如题目问int a[3][4]; printf(%zu %zu %zu\n, sizeof(a), sizeof(a1), sizeof(*(a1)));逐步分析sizeof(a)是 48数组名作用于 sizeof 不退化a1中 a 退化为指针a1的类型是int(*)[4]是指针所以sizeof(a1)是 8*(a1)是对行指针解引用得到int[4]类型的左值数组名没有再进行运算此时sizeof作用于数组类型结果是 16。很多人在sizeof(*(a1))这里犯错以为结果是 4。实际上*(a1)是一个长度为 4 的数组对象不是单个 int。再看一道常见题char *str[] {ab, cde}; printf(%zu %zu\n, sizeof(str), sizeof(str[0]));str是包含 2 个指针的数组sizeof(str)是 1664 位环境下两个指针。str[0]是指针变量sizeof(str[0])是 8。别把它和sizeof(ab)混淆后者是 3包括结尾的空字符。5.3 自己验证的方式笔试准备的阶段我不建议直接背答案建议每个结论都亲手跑一遍。可以写一个简单的测试程序#include stdio.h int main(void) { int a[5] {1, 2, 3, 4, 5}; int *p a; printf(sizeof(a)%zu\n, sizeof(a)); printf(sizeof(p)%zu\n, sizeof(p)); printf(a%p, a%p\n, (void*)a, (void*)a); printf(a1%p, a1%p\n, (void*)(a1), (void*)(a1)); return 0; }输出会直接展示地址值和步长差异。建议你在自己的编译器上把每种声明都打一遍尤其是int *p[5]和int (*p)[5]的sizeof和赋值。提示printf打印指针时建议强转成void*这样符合格式串要求也可以避免某些编译器在%p下报警告。6. 学习建议不要靠背题靠理解内存模型我最想强调的一点是数组名和指针这个问题靠背结论应付不了笔试变化题。面试官稍微改一下操作符、换换维度答案就不同。只有把内存模型画清楚才能真正稳定得分。6.1 画内存图是基本功遇到任何数组指针问题我的习惯是在纸上画格子。比如int a[3][4]画 3 行 4 列的格子每个格子写一个 int。a是整个格子的起始位置a[1]是第二行格子的起始位置a[1][2]是第二行第三列的格子。指针变量的图要画成“一个格子存地址箭头指向目标格子”。数组名的图就是一个区域没有单独的箭头变量。这两个模型一旦在脑子里定型不容易出错。比如int *p a;画出来就是 p 变量里存储了 a 第一行第一个格子的地址。int (*p)[4] a;画出来是 p 指向一整行长度为 4 的数组所以p1跳一行。这个差异用格子图一眼就能看出来。6.2 练习环境练习 C 语言不需要复杂的工具。本地装一个 GCC 或 Clang写一个.c文件用命令行编译运行就够了。很多在线判题系统也支持 C 语言可以快速验证代码输出。如果你在 Windows 上用的是集成开发环境注意编译标准。至少支持 C99 的编译器比较合适因为变长数组和for循环声明等特性会用到。如果编译器版本较老某些写法可能报错不一定是你的逻辑问题也可能是标准太旧。嵌入式方向的同学还要注意交叉编译器的实现和 PC 上的 GCC 可能有差异尤其是sizeof大小、默认对齐方式、位域内存布局。做笔试题时除非题目特别说明一般按照通用 32 位或 64 位平台来算32 位平台指针是 4 字节64 位平台是 8 字节sizeof(int)也不一定总是 4但绝大多数 PC 环境是 4。6.3 常见误区再强调最后把容易反复犯的误区集中列一下误区一a就是a[0]。严格说表达式值上大多数时候相等但a的类型完全不同。a[0]是int*a是int(*)[5]做题时要区分。误区二函数形参里的数组和数组名是一回事。形参里的int arr[]已经被替换成int *arr所以函数里算不出原始长度。误区三任何字符串都是字符数组。字符串字面量是只读的用字符数组初始化和用指针指向字面量可修改性完全不同。误区四int *p[5]和int (*p)[5]读起来差不多。先从优先级和结合性入手[]优先于*所以前者是数组后者是指针。误区五看到地址值一样就认为类型一样。地址值只是内存位置类型决定步长和可执行操作。误区六a 1会越界访问。其实它只是跳到数组末尾之后的位置属于“指向数组之后的下一个位置”的合法指针值但你不能解引用它除非进行强转和恰当偏移。误区七数组下标越界会立刻报错。C 语言不会自动检查下标越界往往导致难以察觉的内存破坏。笔试里可能问结果但实际代码中千万不要依赖这种未定义行为。如果准备笔试时间紧把这些误区对应的代码都自己跑一遍记住输出再理解为什么。如果时间充裕建议从 C 标准里关于数组转换的规则看起配合指针运算和 sizeof 规则一起理解效果会好很多。这不是投机取巧而是把底层逻辑打通以后写嵌入式、操作系统、数据结构相关的 C 代码都会少踩很多坑。