C语言数组名与指针的区别:sizeof、和函数传参的笔试陷阱
2026/8/31 8:35:08 网站建设 项目流程

数组名和指针,是 C 语言笔试里最容易翻车的一对概念。很多人在初学阶段就把“数组名就是指针”挂在嘴边,结果一到考场,遇到sizeof&数组名、函数传参这三类题,立刻错一半。面试官最喜欢在这里挖坑,不是因为题目偏,而是因为这个知识点能真实反映你有没有建立 C 语言的内存模型思维。

这次我们不念概念,直接用代码、内存图和笔试题把“数组名”和“指针”的边界彻底划清楚。

先说结论:数组名是地址常量,不是指针变量;它在绝大多数表达式中会“退化”为指向首元素的指针,但在sizeof&、字符串字面量初始化这三种场景下,它表现出完全不同的行为。搞懂这三条,笔试中 90% 的数组与指针题都不会再丢分。

1. 核心概念速览

考察维度数组名指针变量
本质类型地址常量(不可赋值)变量(可重新指向)
是否占存储空间不单独占用指针变量的存储(数组本身占空间)占用一个指针大小的存储(4/8字节)
sizeof 结果整个数组的字节数指针本身的大小
可否自增/自减不可以(a++ 编译报错)可以(p++ 合法)
可否被赋值不可以(a = p 编译报错)可以(p = a 合法)
& 运算结果指向整个数组的指针(步长为整个数组)指向指针变量的指针
函数传参时退化为指向首元素的指针保留指针语义
经典适用场景数组定义、sizeof 统计、遍历基址动态内存、函数参数、链表遍历

这张表基本覆盖了所有笔试高频考点。下面逐个展开,每个点都配代码验证。

2. 数组名到底是什么

先从一个最基础的例子看起。

#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int *p = a; printf("a = %p\n", (void *)a); printf("&a[0] = %p\n", (void *)&a[0]); printf("p = %p\n", (void *)p); printf("a + 1 = %p\n", (void *)(a + 1)); printf("p + 1 = %p\n", (void *)(p + 1)); return 0; }

在我的测试环境(64 位 Linux + gcc)下,输出如下(地址每次运行会变,但差值不变):

a = 0x7ffd12345670 &a[0] = 0x7ffd12345670 p = 0x7ffd12345670 a + 1 = 0x7ffd12345674 p + 1 = 0x7ffd12345674

这里能确认三件事:

  1. a&a[0]的值完全一样,都是首元素的地址。
  2. a + 1p + 1都跳过了 4 个字节,也就是int的大小。
  3. p是变量,可以随时指向别的地方;a不行,它是常量。

a理解成“首元素的地址”在大部分场景下是对的。但这个理解太粗糙,正是它导致后面sizeof&的坑。

准确的说法是:数组名是一个“指向数组首元素的地址常量”。它本质上是一个常量,不是一个指针变量。C 标准里甚至不要求编译器为数组名单独分配一个指针变量的存储空间,它只是一个表达式层面的地址值。

可以做个实验:如果把数组名当指针变量用,编译器会直接报错。

int a[5] = {1, 2, 3, 4, 5}; int *p = a; a = p; // 编译错误:assignment to expression with array type a++; // 编译错误:lvalue required as increment operand

这两行一旦出现在代码里,gcc 会立刻给出错误。数组名不是左值(准确地说,数组类型的左值在大多数表达式中会退化为指针值,但这个指针值本身不可修改),所以不能出现在赋值运算符的左边。

这里顺便说一下“退化”这个术语。C 标准规定:除了三种情况(sizeof的操作数、&的操作数、字符串字面量用于初始化字符数组)外,类型为“数组”的表达式都会转换为类型为“指向其首元素的指针”的表达式。也就是说,数组名大多数时候确实表现得像一个指针,但这是“退化”后的样子,不是它的完整形态。

3. 数组名和指针的本质区别

很多人困惑:既然ap都指向同一个地址,a[0]p[0]都能取到同一个元素,为什么不能说数组名就是指针?

原因是两者在内存模型上完全不同。

指针变量是一个独立的存储单元。当执行int *p = a;时,系统会在栈上分配 8 个字节(64 位系统)用于存放p这个变量,p的值是数组首元素地址。也就是说,p本身就是内存里的一个对象,它有地址(&p是有意义的)。

数组名不是独立的存储单元。它只是编译器用来表示数组那块内存起始位置的符号,不额外占用空间。数组那块内存(20 字节)是真实存在的,但a本身不另外占 8 字节的指针存储。&a得到的是数组首地址,而不是“存放a这个变量的地址”。

sizeof一眼就能看出差别:

#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)); return 0; }

输出:

sizeof(a) = 20 sizeof(p) = 8

sizeof(a)返回整个数组的字节数(5 × 4 = 20),sizeof(p)返回指针变量本身的大小(64 位系统下是 8)。如果在笔试里看到sizeof(数组名),不要犹豫,它统计的是整个数组,不是地址。

再对比一下&的结果:

#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; printf("a = %p\n", (void *)a); printf("&a = %p\n", (void *)&a); printf("a + 1 = %p\n", (void *)(a + 1)); printf("&a + 1 = %p\n", (void *)(&a + 1)); return 0; }

输出示例:

a = 0x7fff12345670 &a = 0x7fff12345670 a + 1 = 0x7fff12345674 &a + 1 = 0x7fff12345684

注意:a&a的值相同,但类型不同。a + 1只跳过 4 字节(一个int),&a + 1跳过了 20 字节(整个数组)。因为&a的类型是int (*)[5],指向整个数组的指针,它的步长是数组的大小。

这是笔试中选择题最高频的考点:问&a + 1减去a等于多少,或者问*(&a + 1)是什么。答案本质上是“越过整个数组后的那个地址”。

4. 三大笔试高频考点逐项拆解

4.1 sizeof 与数组名

sizeof是笔试的最低门槛,也是区分“初学者背结论”和“真的理解内存模型”的分水岭。

#include <stdio.h> int main(void) { char str[] = "hello"; char *p = "hello"; printf("sizeof(str) = %zu\n", sizeof(str)); printf("strlen(str) = %zu\n", strlen(str)); printf("sizeof(p) = %zu\n", sizeof(p)); printf("strlen(p) = %zu\n", strlen(p)); return 0; }

输出:

sizeof(str) = 6 strlen(str) = 5 sizeof(p) = 8 strlen(p) = 5

str[]是数组,sizeof统计到字符串字面量末尾的\0,所以是 6;p是指针变量,sizeof只统计指针本身,所以是 8。strlen不管底层是数组还是指针,一律数到\0为止,所以都是 5。

笔试常见的变形还有:

int a[10]; printf("%zu\n", sizeof(a)); // 40 printf("%zu\n", sizeof(a + 0)); // 8,a+0 退化为指针 printf("%zu\n", sizeof(*a)); // 4,*a 是首元素

sizeof(a)是数组大小;但一旦a参与了运算(比如a + 0),它就退化成指针,sizeof得到的就是指针大小。这个细节最容易扣分。

4.2 &数组名与数组名

前面已经提过,a&a值相同,类型不同。需要再深挖一层:*(&a)是什么?

#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; printf("a = %p\n", (void *)a); printf("*(&a) = %p\n", (void *)*( &a)); printf("*(&a) + 1= %p\n", (void *)(*(&a) + 1)); return 0; }

*(&a)是对“指向整个数组的指针”解引用,得到的是数组本身(类型为int[5]),但在表达式中它又退化为首元素指针。所以*(&a) + 1跳过一个int,而不是跳过一个数组。这个点在不少面试追问里会出现。

4.3 函数传参时的数组名退化

这是最经典的坑。很多人以为函数参数写成int arr[]就真的传了一个数组进去,但 C 语言函数参数中所有数组形参都会退化为指针。

#include <stdio.h> void test(int arr[]) { printf("sizeof(arr) = %zu\n", sizeof(arr)); } int main(void) { int a[10] = {0}; printf("sizeof(a) = %zu\n", sizeof(a)); test(a); return 0; }

输出:

sizeof(a) = 40 sizeof(arr) = 8

mainsizeof(a)是 40,进了函数变成 8。因为int arr[]在参数列表里等价于int *arr。数组没有按值传递,传过去的只是首元素指针。这也是为什么在函数里计算参数数组长度必须额外传一个len,不能靠sizeof推断。

如果要把“整个数组”传给函数,只有一种方式:传递指向整个数组的指针。

#include <stdio.h> void test2(int (*p)[10]) { printf("sizeof(*p) = %zu\n", sizeof(*p)); } int main(void) { int a[10] = {0}; test2(&a); return 0; }

这里p的类型是int (*)[10]*p是数组本身,所以sizeof(*p)重新得到了 40。但这种写法在实际项目中不多,因为把数组长度写死在类型里,灵活性很差。

5. 多维数组:行指针与列指针别再混了

多维数组的数组名退化逻辑,笔试中出现频率更高。先看一个二维数组:

#include <stdio.h> int main(void) { int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; printf("arr = %p\n", (void *)arr); printf("arr + 1 = %p\n", (void *)(arr + 1)); printf("*arr = %p\n", (void *)*arr); printf("*arr + 1 = %p\n", (void *)(*arr + 1)); printf("arr[1] = %p\n", (void *)arr[1]); printf("&arr[1] = %p\n", (void *)&arr[1]); printf("&arr[1][0]= %p\n", (void *)&arr[1][0]); return 0; }

输出示例(差值恒定):

arr = 0x7fff0000 arr + 1 = 0x7fff0010 // 跳了 16 字节,一行 *arr = 0x7fff0000 *arr + 1 = 0x7fff0004 // 跳了 4 字节,一个元素 arr[1] = 0x7fff0010 &arr[1] = 0x7fff0010 &arr[1][0]= 0x7fff0010

关键结论:

  • arr的类型是int (*)[4],指向一行(4 个 int)。
  • arr + 1跳一行,也就是 16 字节。
  • *arr的类型是int *,指向行内第一个元素。
  • *arr + 1跳一个元素,也就是 4 字节。
  • arr[1]&arr[1]&arr[1][0]值相同,但前两者的类型是int *,第三个是int *,在表达式中都指向第二行行首。

所以二维数组的数组名退化后是一个“行指针”,而不是普通元素指针。要用指针遍历二维数组,必须明确你的指针到底是“指向一行的指针”还是“指向一个元素的指针”。

#include <stdio.h> int main(void) { int arr[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; int (*row)[4] = arr; int *ele = &arr[0][0]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { printf("%d ", row[i][j]); // 等价写法:*(*(row + i) + j) } } printf("\n"); for (int i = 0; i < 12; i++) { printf("%d ", *(ele + i)); } printf("\n"); return 0; }

row是行指针,用row[i][j]访问;ele是元素指针,用*(ele + i)线性访问。两者都能遍历,但类型不同,混用会让编译器告警,语义上也容易出错。

面试中还有一个常见追问:arr[i][j]的等价写法。从指针角度看:

  • arr[i]等价于*(arr + i)
  • arr[i][j]等价于*(*(arr + i) + j)

有些笔试会让写出*(*(arr + 1) + 2)的值,其实就是arr[1][2],即 7(按上面的数组内容)。

6. 字符串数组与字符指针的经典陷阱

字符串在 C 语言里没有独立类型,只能通过字符数组或字符指针来使用。这两种方式在内存分布上完全不同,笔试很喜欢把两者放在一起考。

#include <stdio.h> #include <string.h> int main(void) { char str1[] = "hello"; char *str2 = "hello"; printf("sizeof(str1) = %zu\n", sizeof(str1)); // 6,包含 '\0' printf("sizeof(str2) = %zu\n", sizeof(str2)); // 8,指针大小 str1[0] = 'H'; // 合法:字符数组可修改 printf("str1 = %s\n", str1); // str2[0] = 'H'; // 编译通过,运行崩溃:字符串字面量是只读区 return 0; }

str1是字符数组,内存位于栈上,内容可以修改;str2指向字符串字面量,字面量通常存放在只读数据段,尝试修改会触发运行时错误(Segmentation Fault)。很多初学者看到两者都能打印hello,就以为是一样的,实际一个可写一个只读。

笔试还有一类题是判断strlensizeof的组合:

char *p = "hello world"; printf("%zu %zu\n", sizeof(p), strlen(p));

输出是8 11sizeof(p)是指针大小,strlen(p)是字符串长度。如果把char *p换成char p[] = "hello world";sizeof(p)就变成 12(包含\0)。

要注意的另一个点是:char *p = "hello"这种写法在 C++ 里是不合法的(C++11 起要求加const),但在 C 语言里可以编译,只是写入会崩溃。笔试题如果需要严谨表述,应该写const char *p = "hello";

7. 指针数组、数组指针与函数指针

笔试还喜欢把“指针数组”和“数组指针”放在一起考。两个词的顺序不同,含义完全不同。

int *p[5]; // 指针数组:一个数组,里面存了 5 个 int* int (*p)[5]; // 数组指针:一个指针,指向含有 5 个 int 的数组

判断方法是看p先和谁结合:

  • int *p[5]p先和[5]结合,所以p是数组,元素类型是int *
  • int (*p)[5]()改变了结合顺序,p先和解引用*结合,所以p是指针,指向int[5]这个数组。

简单记忆:

  • 指针数组= 数组,里面放指针。
  • 数组指针= 指针,指向数组。
#include <stdio.h> int main(void) { int a = 1, b = 2, c = 3; int *arr[3] = {&a, &b, &c}; for (int i = 0; i < 3; i++) { printf("%d ", *arr[i]); } printf("\n"); int nums[2][3] = {{1, 2, 3}, {4, 5, 6}}; int (*p)[3] = nums; printf("p[1][2] = %d\n", p[1][2]); return 0; }

如果面试题里出现char *argv[],这就是一个”指针数组”,数组元素是char *。命令行参数就是通过这种方式传给main函数的。

函数指针在数组名这个话题里属于进阶考点,但和数组名有一个共同点:函数名在表达式中也会退化为函数指针。比如:

#include <stdio.h> int add(int a, int b) { return a + b; } int main(void) { int (*fp)(int, int) = add; // add 退化为函数指针 int result = fp(3, 4); printf("%d\n", result); return 0; }

add&add的值相同,类型都是int (*)(int, int)。这和数组名的行为类似,但函数指针没有sizeof数组长度这类坑,考点主要是调用形式和类型声明。

8. 笔试典型题实战解析

这一节整理几道高频笔试题,先不要看答案,自己在草稿纸上推一遍。

题 1

#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int *p = a; printf("%d\n", *(p + 1)); printf("%d\n", *(a + 2)); printf("%zu\n", sizeof(a)); printf("%zu\n", sizeof(p)); return 0; }

依次输出:

  • *(p + 1)p指向首元素,加 1 指向第二个元素,解引用得到 2。
  • *(a + 2)a + 2是第三个元素的地址,解引用得到 3。
  • sizeof(a):数组大小,20。
  • sizeof(p):指针大小,8。

题 2

#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int *p = (int *)(&a + 1); printf("%d\n", *(p - 1)); return 0; }

&a的类型是int (*)[5]&a + 1指向数组末尾之后的位置。强制转成int *后,p - 1回退一个int,指向最后一个元素,所以输出 5。

这题考查两个点:&a的步长是整个数组;p - 1是按int步长回退。

题 3

#include <stdio.h> int main(void) { int a[3][4] = {0}; printf("%zu\n", sizeof(a)); printf("%zu\n", sizeof(a[0])); printf("%zu\n", sizeof(a[0][0])); return 0; }
  • sizeof(a):整个二维数组,3 × 4 × 4 = 48。
  • sizeof(a[0]):第一行数组,4 × 4 = 16。
  • sizeof(a[0][0]):一个int,4。

题 4

#include <stdio.h> void foo(int arr[100]) { printf("%zu\n", sizeof(arr)); } int main(void) { int a[100] = {0}; foo(a); return 0; }

函数参数里的int arr[100]等价于int *arr,所以sizeof(arr)是指针大小,8。数组长度 100 在这里只是一个提示性写法,编译器不检查。

题 5

#include <stdio.h> int main(void) { char *str[] = {"apple", "banana", "cherry"}; printf("%zu\n", sizeof(str)); printf("%zu\n", sizeof(str[0])); printf("%c\n", *(str[1] + 1)); return 0; }

str是一个指针数组,包含 3 个char *sizeof(str)是 3 × 8 = 24,sizeof(str[0])是指针大小 8,str[1]指向"banana",加 1 指向'a',所以输出a

指针数组的sizeof和字符数组的sizeof很容易混,只要记住:数组里存的是指针,统计的就是指针总量。

9. 调试方法与内存模型验证

如果你在 IDE 里调试这些代码,光看输出可能还是觉得抽象。推荐用 gdb 直接观察地址和类型,能把“数组名是常量”这件事看得更清楚。

gcc -g -o demo demo.c gdb ./demo

在 gdb 里输入:

break main run print a print &a print p print &p print sizeof(a) print sizeof(p)

print &p会显示指针变量的地址,说明它是一个真实存在的对象;print &a显示的地址和print a一样,但类型不同,gdb 会显示int (*)[5]。这就直接验证了“数组名不额外占用指针存储,但类型信息完整保留”。

如果不想用 gdb,也可以在代码里输出&p

#include <stdio.h> int main(void) { int a[5] = {1, 2, 3, 4, 5}; int *p = a; printf("p = %p\n", (void *)p); printf("&p = %p\n", (void *)&p); return 0; }

p&p的地址不同,说明p变量本身有存储位置。而数组名a没有对应的“存储位置”,因为编译器不会为它分配一个指针变量。

10. 常见问题与排查清单

问题现象可能原因排查方式解决方案
sizeof(数组名)结果不是数组总字节数数组名已经退化为指针,比如作为函数参数传入检查是否在函数内部使用 sizeof额外传入数组长度,或用宏ARRAY_SIZE(a)
a++编译报错数组名是地址常量,不是左值查看编译器错误信息改用指针变量int *p = a; p++;
修改字符串字面量导致段错误字符指针指向只读区,误当可写数组gdb 查看崩溃位置使用字符数组char str[] = "..."或加const
&a + 1的地址和预期不符&a的类型是数组指针,步长是整个数组打印地址差值明确类型,按步长换算
函数内sizeof数组参数结果不对数组参数退化为指针打印sizeof(arr)不要用 sizeof 推断长度
int *p[5]int (*p)[5]声明混用把数组指针和指针数组搞反decltype/编译器告警辅助记住结合优先级

面试还有一种常见追问,就是“如何安全计算数组元素个数”。推荐的写法是:

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

但它对函数参数无效,因为函数参数中的a已经退化为指针。所以:

int a[10]; // 正确场景 printf("%zu\n", ARRAY_SIZE(a)); // 10 void foo(int arr[]) { // 错误场景 // printf("%zu\n", ARRAY_SIZE(arr)); // 得到的是指针数组的比例,结果错误 }

11. 记忆口诀与实战建议

数组名和指针的区别,如果只记一句话,就记:数组名是地址常量,不是变量,但绝大多数运算中它会退化成首元素指针;sizeof&是少数不退化的场景。

再展开成笔试口诀:

  1. sizeof(数组名)是整个数组大小;sizeof(指针)是指针大小。
  2. a + 1跳一个元素;&a + 1跳一个数组。
  3. 函数参数里写int arr[]等于写int *arr
  4. 二维数组名退化后是行指针,步长是一行。
  5. int *p[5]是指针数组,int (*p)[5]是数组指针。
  6. 可修改的字符串用char str[],不可修改的字符串用const char *str

笔试高频题型基本都在这六条里。

实际项目的建议是:能不用指针运算的地方,尽量用下标访问;必须用指针遍历时,先明确指针的类型指向的是“元素”还是“一行/一个数组”;凡是涉及字符串字面量,最好都用const char *。更重要的是,编译器告警不要忽略,比如-Wincompatible-pointer-types这类告警往往能提醒你把int *int (*)[N]搞混了。

编译时建议开启-Wall -Wextra

gcc -Wall -Wextra -o demo demo.c

这样可以尽早暴露类型不匹配问题。笔试考的是概念,工程里考的是习惯,两件事一起抓好,C 语言这块就不会再翻车。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询