ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言数组名与指针的区别:sizeof、和函数传参的笔试陷阱

C语言数组名与指针的区别:sizeof、和函数传参的笔试陷阱 数组名和指针是 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这里能确认三件事a和a[0]的值完全一样都是首元素的地址。a 1和p 1都跳过了 4 个字节也就是int的大小。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. 数组名和指针的本质区别很多人困惑既然a和p都指向同一个地址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) 8sizeof(a)返回整个数组的字节数5 × 4 20sizeof(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 字节一个inta 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) 5str[]是数组sizeof统计到字符串字面量末尾的\0所以是 6p是指针变量sizeof只统计指针本身所以是 8。strlen不管底层是数组还是指针一律数到\0为止所以都是 5。笔试常见的变形还有int a[10]; printf(%zu\n, sizeof(a)); // 40 printf(%zu\n, sizeof(a 0)); // 8a0 退化为指针 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在main里sizeof(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就以为是一样的实际一个可写一个只读。笔试还有一类题是判断strlen和sizeof的组合char *p hello world; printf(%zu %zu\n, sizeof(p), strlen(p));输出是8 11。sizeof(p)是指针大小strlen(p)是字符串长度。如果把char *p换成char p[] hello world;sizeof(p)就变成 12包含\0。要注意的另一个点是char *p hello这种写法在 C 里是不合法的C11 起要求加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])一个int4。题 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 24sizeof(str[0])是指针大小 8str[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[] ...或加consta 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和是少数不退化的场景。再展开成笔试口诀sizeof(数组名)是整个数组大小sizeof(指针)是指针大小。a 1跳一个元素a 1跳一个数组。函数参数里写int arr[]等于写int *arr。二维数组名退化后是行指针步长是一行。int *p[5]是指针数组int (*p)[5]是数组指针。可修改的字符串用char str[]不可修改的字符串用const char *str。笔试高频题型基本都在这六条里。实际项目的建议是能不用指针运算的地方尽量用下标访问必须用指针遍历时先明确指针的类型指向的是“元素”还是“一行/一个数组”凡是涉及字符串字面量最好都用const char *。更重要的是编译器告警不要忽略比如-Wincompatible-pointer-types这类告警往往能提醒你把int *和int (*)[N]搞混了。编译时建议开启-Wall -Wextragcc -Wall -Wextra -o demo demo.c这样可以尽早暴露类型不匹配问题。笔试考的是概念工程里考的是习惯两件事一起抓好C 语言这块就不会再翻车。
返回列表