尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C语言数组名与指针区别:sizeof、a+1与二维数组陷阱详解

C语言数组名与指针区别:sizeof、a+1与二维数组陷阱详解 1. 先说结论数组名不是指针笔试就爱考这里C 语言里最容易被误解的两个概念排第一的绝对是“数组名”和“指针”。很多人学完指针之后下意识把数组名当成一个指向首元素的指针常量写代码倒是能跑但一到笔试、面试题里就翻车——sizeof算错、a 1算错、函数传参后长度丢失、二维数组和int **混用编译直接警告。这个问题的根源在于数组名在“大多数表达式中”会退化成指向首元素的指针但在另外一些场景里它又保持数组身份。理解这一点需要回到 C 语言对数组的底层定义而不是死记结论。这篇文章要讲清楚四件事数组名到底是什么什么时候退化、什么时候不退化。sizeof(a)、a 1、*(a 1)这类表达式在笔试中怎么算。函数参数为什么收不到真正的数组int arr[]和int *arr为什么完全相同。二维数组、数组指针、指针数组之间的层次关系。第 2 和第 3 点是最常挖坑的位置也是网上讨论最激烈的地方。看完之后建议立刻拿编译器和调试器验证一遍所有结论都能在本地复现。2. 核心知识点速查表知识点结论典型场景数组名的本质数组名代表整个数组对象不是指针变量int a[5];中a的类型是int[5]数组名退化为指针除sizeof、、字符串字面量初始化等场景外a退化为a[0]算术运算、函数传参sizeof(a)返回整个数组占用的字节数sizeof(int[5])在 32 位 int 下是 20sizeof(a 1)先退化再计算结果是指针大小a 1的类型是int *a类型是int(*)[5]指向整个数组a 1跳过整个数组a[0]类型是int *指向首元素a[0] 1指向第二个元素函数形参int arr[]完全等同于int *arr数组传参时长度丢失二维数组名退化为指向第一行的数组指针不是int **int a[3][4]退化为int (*)[4]数组指针int (*p)[N]p 指向一个长度为 N 的数组p a;指针数组int *p[N]p 是由 N 个指针组成的数组char *argv[]下面的每个小节都会展开讲但建议先把这张表背下来再去看推导过程。3. 数组名的本质先看 C 语言怎么定义数组C 语言中数组是一块连续的、元素类型相同的内存区域。比如int a[5] {1, 2, 3, 4, 5};变量a的类型是int[5]它占据 5 个int长度的连续内存。这一整段内存就是数组本身。数组名a不是单独存储的一个指针变量它代表的是“整个数组对象”。这里有一个容易混淆的细节打印a的值会发现它和a[0]相同都是首元素地址。但这只说明“值相同”不代表“类型相同”。指针变量和数组名的本质差别在于指针变量有自己独立的内存空间里面保存一个地址。数组名没有自己额外的存储空间它在编译期被解析成数组首地址相关的表达式。不能对数组名执行a、a p这类赋值或自增操作因为它不是一个左值指针变量。int a[5]; int *p; p a; // 合法a 退化为指针 a p; // 编译错误a 不是可修改的左值 a; // 编译错误不符合语法 p; // 合法p 指向下一个元素所以更准确的说法是数组名在大多数表达式中会被隐式转换成“指向首元素的指针”而不是数组名本身就是指针。这正是 C 语言标准中数组到指针的退化规则array decay。3.1 数组名什么时候不退化根据 C 标准数组名在以下三种主要场景中不退化作为sizeof的操作数时。作为的操作数时。用字符串字面量初始化字符数组时。#include stdio.h int main(void) { int a[5] {1, 2, 3, 4, 5}; printf(%zu\n, sizeof(a)); // 20整个数组大小假设 int 占 4 字节 printf(%p\n, (void *)a); // 数组地址类型是 int(*)[5] printf(%p\n, (void *)a); // 首元素地址类型是 int* printf(%p\n, (void *)a[0]); // 首元素地址类型是 int* return 0; }注意printf(%p)需要把指针转成void *否则在绝大多数平台上虽然能输出地址但严格来说是未定义行为笔试里碰到也要留个心眼。第一次输出的sizeof(a)是 20而如果a是一个指针变量sizeof只会返回 8 或者 4指针大小。这是区分“数组名”和“指针”最直接的证据。4.sizeof、取地址、指针加减笔试最容易错的三组表达式4.1sizeof系列笔试题先看一段经典代码#include stdio.h int main(void) { int a[5] {1, 2, 3, 4, 5}; int *p a; printf(%zu\n, sizeof(a)); // 20 printf(%zu\n, sizeof(p)); // 864位平台 printf(%zu\n, sizeof(*p)); // 4 printf(%zu\n, sizeof(a 1)); // 8a 先退化a1 是 int* printf(%zu\n, sizeof(a)); // 8指向数组的指针 printf(%zu\n, sizeof(a 1)); // 8仍然是数组指针 printf(%zu\n, sizeof(a[0])); // 4 return 0; }每一个输出都值得拆开说sizeof(a)a没有退化计算整个数组的大小5 * 4 20。sizeof(p)p是指针变量返回指针自身大小64 位平台是 8。sizeof(*p)p指向int解引用后类型是int大小是 4。sizeof(a 1)a 1表达式中a退化为指针结果为int *指针大小 8。这里的加 1 是让指针指向第二个元素但sizeof关心的是表达式类型不是值。sizeof(a)a的类型是int(*)[5]是指针所以大小是 8不是 20。sizeof(a[0])a[0]类型是int大小是 4。容易混淆的一个点是sizeof(a)看起来“好像包含整个数组”它确实指向整个数组但指针本身只占 8 字节。指针变量保存的是地址地址不会因为指向的对象很大就变大。4.2a 1到底跳了多少这是笔试出现频率非常高的一道题#include stdio.h int main(void) { int a[5] {1, 2, 3, 4, 5}; int *p (int *)(a 1); printf(%d\n, *(p - 1)); // 输出 5 return 0; }分析过程a的类型是int[5]a的类型是int(*)[5]。对int(*)[5]类型的指针执行1指针会跳过一个完整数组即跳过sizeof(int[5])也就是 20 字节。(int *)(a 1)把这个地址转换成int *。此时p指向数组末尾后面第 1 个int位置p - 1指向最后一个元素解引用得到 5。如果把题目改成int *p a 1;那p - 1指向的是第一个元素输出就是 1。对比两条代码能直观看出a 1和a 1的步长差异a 1以sizeof(int)为步长。a 1以sizeof(int[5])为步长。这个差异在所有数组中一致包括多维数组。4.3 下标运算的等价转换C 语言里a[i]和*(a i)完全等价并且这个等价关系对指针也成立。也就是说p[3] 等价于 *(p 3) 3[p] 等价于 *(3 p)最后一个表达式3[p]在语法上合法因为下标运算符的两个操作数一个是指针、一个是整数左右顺序并不固定。这属于 C 语言的历史遗留写法笔试偶尔会出现但实际工程里不要这么写代码可读性会变差。5. 函数参数中的数组长度为什么传丢了再看一个经典问题#include stdio.h void test(int arr[10]) { printf(%zu\n, sizeof(arr)); } int main(void) { int a[10] {0}; printf(%zu\n, sizeof(a)); // 40 test(a); // 864位平台 return 0; }函数内输出的sizeof(arr)不是 40而是 8 或者 4。原因在于C 语言规定函数形参声明中的数组类型会被调整成对应的指针类型void test(int arr[10]); // 完全等价于 void test(int *arr);甚至写int arr[]、int arr[100]在形参声明中含义都一样全是int *arr。这意味着实参数组传入函数后函数内部拿到的只是一个指向首元素的指针数组的长度信息不会自动跟着传过去。所以 C 语言里所有“需要在函数中使用数组长度”的场景都必须额外传长度参数#include stdio.h int sum_array(const int *arr, int len) { int sum 0; for (int i 0; i len; i) { sum arr[i]; } return sum; } int main(void) { int a[5] {1, 2, 3, 4, 5}; int total sum_array(a, 5); printf(%d\n, total); return 0; }这里a在传参时退化成指针函数内部不可能通过sizeof(arr) / sizeof(arr[0])算出数组长度必须靠显式传入的len。5.1 为什么不能在函数里用sizeof(arr) / sizeof(arr[0])因为sizeof(arr)得到的是指针大小sizeof(arr[0])得到的是目标元素大小两者的商在 64 位平台上通常是8 / 4 2这个结果没有任何意义。笔试和工程中这是一类非常经典的 bugint get_array_len(const int arr[]) { // 错误示例 // return sizeof(arr) / sizeof(arr[0]); }正确做法就是外传长度或者使用 C99 变长数组参数、结构体封装数组等方案。6. 二维数组名与int **类型层次容易懵二维数组名退化的目标是数组指针不是二级指针。很多初学者以为int a[2][3]传参可以写成int **a其实编译时会报警告运行结果也完全对不上。int a[2][3] {{1, 2, 3}, {4, 5, 6}}; int **p a; // 编译警告类型不兼容原因在于a的类型是int[2][3]。在大多数表达式中a退化为指向第一行的指针也就是int (*)[3]。int (*)[3]指向的是一维数组int[3]而int **指向的是int *两者指向的目标结构完全不同。正确的函数参数声明有两种写法等价void print_matrix_1(int m[][3], int rows) { for (int i 0; i rows; i) { for (int j 0; j 3; j) { printf(%d , m[i][j]); } printf(\n); } } void print_matrix_2(int (*m)[3], int rows) { for (int i 0; i rows; i) { for (int j 0; j 3; j) { printf(%d , m[i][j]); } printf(\n); } }调用时直接传数组名即可print_matrix_1(a, 2); print_matrix_2(a, 2);6.1 二维数组加法的步长int a[2][3] {{1, 2, 3}, {4, 5, 6}}; printf(%p\n, (void *)a); // 第一行首地址 printf(%p\n, (void *)(a 1)); // 第二行首地址相差 12 字节 printf(%p\n, (void *)(a[0] 1)); // 第一行第二个元素相差 4 字节这里的关键区分a 1a退化为int(*)[3]加 1 跳过整行即3 * sizeof(int)。a[0] 1a[0]类型是int[3]退化后是int *加 1 只跳过一个int。a[0]和a[1]是一维数组名它们退化成指针后分别指向两行的首元素。6.2sizeof对二维数组的处理sizeof(a); // 2 * 3 * 4 24 sizeof(a[0]); // 3 * 4 12 sizeof(a[0][0]);// 4这三个结果和第二维大小有关第二维固定为 3 时sizeof(a[0])才能正确算出一行的大小。这也是为什么二维数组作为函数参数时第二维不能省略void f(int arr[][3], int rows); // 正确 void f(int arr[][], int rows); // 编译错误第二维未知无法计算步长7. 指针数组与数组指针需要区分两个名字接近的概念“指针数组”和“数组指针”是笔试常考的概念辨析int *p[3]; // p 先和 [] 结合是数组元素类型是 int* int (*q)[3]; // q 先和 * 结合是指针指向 int[3]处理这两个声明有一个通用思路找变量名看它先和谁结合。p先和[3]结合说明p是一个数组数组里有 3 个元素每个元素是int *类型。q先和*结合说明q是一个指针它指向的元素类型是int[3]。指针数组的典型使用场景#include stdio.h int main(void) { int x 1, y 2, z 3; int *p[3] {x, y, z}; for (int i 0; i 3; i) { printf(%d , *p[i]); } printf(\n); return 0; }数组指针的典型使用场景就是前面讲到的二维数组行指针int a[2][3] {{1, 2, 3}, {4, 5, 6}}; int (*q)[3] a; // 指向第一行7.1char *argv[]为什么是指针数组main函数的第二个参数argv类型是char *argv[]也就是一个指针数组数组的每个元素是char *字符串指针。它也可以写成char **argv因为作为函数形参时数组形式会退化为指针形式int main(int argc, char *argv[]); // 等价于 int main(int argc, char **argv);这一点和之前讲的“数组形参退化为指针”完全一致。8. 经典笔试题逐题拆解这里整理几道高重复度的笔试/面试题建议先自己写出答案再对照分析。8.1 题目一int a[5] {1, 2, 3, 4, 5}; int *p (int *)(a 1); printf(%d\n, *(a 1)); printf(%d\n, *(p - 1));答案2和5。*(a 1)中a退化为int *加 1 指向第二个元素所以是 2。a 1跳过整个数组转成int *后减 1 指向数组最后一个元素所以是 5。8.2 题目二int a[3][4] {0}; printf(%zu\n, sizeof(a)); printf(%zu\n, sizeof(a[0])); printf(%zu\n, sizeof(a[0][0]));假设 int 占 4 字节且无额外对齐影响。答案48、16、4。sizeof(a)3 行 * 4 列 * 4 字节 48。sizeof(a[0])第一行是一个int[4]大小 16。sizeof(a[0][0])单个int大小 4。8.3 题目三void fun(int arr[10]) { printf(%zu\n, sizeof(arr)); }答案64 位平台输出 832 位平台输出 4。函数形参中的int arr[10]被调整为int *arr。8.4 题目四char *str[] {hello, world}; char **p str; printf(%c\n, **p);答案h。str是指针数组元素类型是char *str[0]是字符串hello的地址。str作为实参传给char **p时退化为二重指针。*p得到str[0]**p解引用得到字符串第一个字符h。注意这里和二维数组不同char *str[]的内部结构是连续存放的指针所以它可以退化成char **而int a[3][4]内部连续存放的是int数据退化成int (*)[4]。两者不能混用。8.5 题目五extern声明不一致的坑一个源文件定义数组另一个源文件却用指针声明接收// a.c int arr[10] {0}; // b.c #include stdio.h extern int *arr; // 错误类型不一致 int main(void) { printf(%d\n, arr[0]); // 未定义行为 return 0; }arr在定义处是数组在声明处被写成了int *这种类型不一致会导致数据读取完全错误。数组和指针在“内存布局”上不是一回事不能因为值相同就随意换用。笔试或面试如果提到多文件编译的问题这通常是隐含考点。9. 常见误区和排查方法这一节把平时答疑中见到的高频误区和排查思路列出来。这里的“排查”不是指编译报错而是指理解代码行为时容易出错的点。误区实际情况验证方法数组名是指针常量数组名代表数组对象表达式中退化为指针sizeof(a)和sizeof(p)结果不同a 1和a 1一样步长不同前者跳一个元素后者跳一个数组打印地址差值函数参数int a[]会传长度形参是int *长度丢失函数内sizeof(a)是 8 或 4二维数组可以传给int **退化成int (*)[4]类型不兼容编译看看是否有警告sizeof在函数内能算出数组长度数组退化为指针算不出打印sizeof(arr)/sizeof(arr[0])数组名可以自增数组名不可修改a编译报错char *argv[]和char **argv无关函数形参中完全等价两种声明都能编译如果你在本地复现结果和预期不一致按顺序检查四点平台位数。64 位平台上指针是 8 字节32 位平台是 4 字节。int大小。大多数 PC 平台是 4 字节嵌入式平台可能是 2 字节。有没有在函数形参里使用sizeof。一旦进入函数数组已经不是数组了。类型转换有没有改变指针运算步长。比如(int *)(a 1)先跳数组再转指针顺序不能反过来。10. 最佳实践与使用建议数组名和指针不是一回事这个结论不只是为了应付笔试在实际编码中同样重要。1. 传数组给函数时永远带上长度。void process(int arr[], size_t len);不要试图在函数内部用sizeof推导长度。这是 C 语言数组设计的一个长期痛点与其踩坑不如约定成俗。2. 需要精确控制数组操作时优先使用指向数组的指针。void fill(int (*arr)[5], int value) { for (int i 0; i 5; i) { (*arr)[i] value; } }这里*arr访问的是整个一维数组(*arr)[i]是数组元素赋值。括号不能丢否则*arr[i]会被解析成*(arr[i])。3. 区分const指向关系。const char *p; // 指向的字符不可改指针可改 char *const p; // 指针不可改指向的字符可改 const char *const p; // 都不可改搞清楚数组和指针的关系之后const修饰的优先级也要同时掌握否则面试题换一层包装还是容易错。4. 二维数组传参时第二维必须明确。void f(int a[][4], int rows); void f(int (*a)[4], int rows);第二维影响指针步长。如果省略第二维编译器无法生成正确的地址计算代码。5. 不要在没有类型转换的情况下直接互换数组和指针。无论是extern声明还是函数参数数组和指针类型不一致时代码可能编译通过但运行结果错误。这类 bug 排查成本非常高最好的方式是在声明阶段保持一致。11. 总结与下一步数组名和指针不是一回事这句话值得反复强调。数组名是数组对象它在绝大多数表达式中退化为指针但在sizeof、取地址、字符数组初始化等场景中保留数组身份。退化规则导致了一系列笔试高频坑sizeof算错、a 1跳错、函数传参长度丢失、二维数组和int **混淆。建议你先做的验证本机编译第一节到第八节的所有代码把printf结果和对照答案逐一比对。在函数内部再一次调用sizeof(arr)亲眼看一次 8 或 4 的输出。把a 1改成a 1观察地址差值的不同。最容易踩的坑就是函数传参后的sizeof以及二维数组名传给int **时的编译警告这两个问题理解了数组和指针的大多数笔试题都能过关。下一步可以考虑继续看这几个方向C 语言中多维数组的内存布局和指针运算。函数指针与回调函数的设计模式。restrict、const、volatile在数组/指针类型中的组合规则。C11 变长数组与动态分配二维数组的区别。数组和指针本质是 C 语言的底层内存模型问题把这块理清后面学数据结构、看 linux 内核源码都会顺畅很多。建议先把这篇文章里的代码全部跑一遍再去做题。
返回列表