C语言指针5:sizeof与strlen对比、数组名三大规则、指针运算笔试题,一次把指针的“终极考点“讲透
个人主页GeekZHR 欢迎各位大佬互三25级机器人工程N非本科生在读自学C/C嵌入式软硬件等等ing欢迎各位大佬评论区互动交流写在前面这一讲是把指针当考试题来考前面四讲我们把地址 指针变量数组名 首元素地址函数指针、回调函数一路打通。这一讲是收官也是一次大考指针不再给你讲新概念而是用一道道笔试题把你前几讲的知识全揉进去看你到底懂没懂。很多人一到sizeof、strlen、a1、char***cpp就开始蒙其实所有题的钥匙只有两把——第一把sizeof和strlen是两回事第二把数组名什么时候代表整个数组、什么时候代表首元素地址。抓住这两把钥匙sizeof/strlen 对比 → 数组名三大规则 → 数组笔试题 → 指针运算笔试题就全串起来了。这篇复盘我按这个顺序把每一道题的答案和推导过程都写清楚配上图给指针系列收个尾。 环境约定本讲笔试题默认在x8632 位环境下讨论——int占 4 字节指针占 4 字节。换到 64 位环境指针相关的sizeof结果会翻倍4 → 8读题时先确认环境。一、sizeof 和 strlen 的对比这是整讲的地基先把这两个长得像、经常被混为一谈的东西彻底分清楚。1.1 sizeof 是操作符算内存大小sizeof是操作符不是函数计算操作数所占内存空间的大小单位是字节。如果操作数是类型算的就是用这个类型创建的变量所占内存大小。关键就一句sizeof只关心占多大内存根本不关心内存里存的是什么数据。#include stdio.h int main() { int a 10; printf(%d\n, sizeof(a)); // 4变量 a 占 4 字节 printf(%d\n, sizeof a); // 4sizeof 是操作符求变量大小可以不加括号 printf(%d\n, sizeof(int)); // 4求类型只能加括号 return 0; }两个小细节sizeof a能不加括号正说明它是操作符而不是函数而sizeof(int)求类型时必须加括号。1.2 strlen 是库函数算字符串长度strlen是 C 语言的库函数用前要#include string.h作用是求字符串长度。函数原型size_t strlen(const char *str);它统计的是从str这个地址开始一直向后到\0之前字符的个数。注意一直向后找\0这句——如果内存里没有\0它就会一直找下去直到碰巧遇到一个\0为止所以可能存在越界查找。看个对比#include stdio.h #include string.h int main() { char arr1[3] {a, b, c}; // 没有 \0 char arr2[] abc; // 实际是 a,b,c,\0共 4 字节 printf(%d\n, strlen(arr1)); // 随机值arr1 里没有 \0会一直往后越界找 printf(%d\n, strlen(arr2)); // 3找到 c 后面的 \0 就停 printf(%d\n, sizeof(arr1)); // 3数组总共 3 个 char printf(%d\n, sizeof(arr2)); // 4字符串字面量自带一个 \0 return 0; }同一个数组strlen(arr1)是随机值、sizeof(arr1)是 3strlen(arr2)是 3、sizeof(arr2)却是 4。为什么因为字符串abc存进数组时末尾会自动补一个\0所以arr2占 4 字节但字符串长度只算\0之前的 3 个字符。1.3 一张表看清区别对比项sizeofstrlen是什么操作符库函数要#include string.h算的是什么操作数所占内存大小字节字符串长度\0之前的字符个数关注数据吗不关注内存里存什么关注内存里有没有\0越界风险无编译期/按类型算有没\0会一直向后找 一句话sizeof是量尺子只量这块内存多大不管里面装的啥strlen是数数从首地址开始数到\0停没\0就数过头。二、数组名三大规则本讲的总开关后面所有题最后都会归结到数组名到底代表什么。先把这条总开关焊死数组名的意义只有三种情况sizeof(数组名)—— 这里的数组名代表整个数组算的是整个数组的大小数组名—— 这里的数组名也代表整个数组取出的是整个数组的地址除此之外数组名一律表示首元素的地址。抓住这三条sizeof里那些为什么sizeof(arr)是 16、sizeof(arr0)是 4就全通了——arr单独出现是整个数组而arr0、arr1、*arr里的arr都退化成首元素地址。IMG2三、数组和指针笔试题解析下面进入实战。所有题默认 x8632 位int4 字节、指针 4 字节、char1 字节。3.1 一维数组int a[] {1,2,3,4}; // 4 个 int数组共 16 字节 printf(%d\n, sizeof(a)); // ① printf(%d\n, sizeof(a0)); // ② printf(%d\n, sizeof(*a)); // ③ printf(%d\n, sizeof(a1)); // ④ printf(%d\n, sizeof(a[1])); // ⑤ printf(%d\n, sizeof(a)); // ⑥ printf(%d\n, sizeof(*a)); // ⑦ printf(%d\n, sizeof(a1)); // ⑧ printf(%d\n, sizeof(a[0])); // ⑨ printf(%d\n, sizeof(a[0]1)); // ⑩逐条拆①sizeof(a)16。a单独放在sizeof里代表整个数组4 个int共 16 字节。②sizeof(a0)4。a0里a是首元素地址0还是首元素地址是指针4 字节。③sizeof(*a)4。*a解引用首元素地址拿到a[0]这个int4 字节。④sizeof(a1)4。a1是第二个元素的地址指针4 字节。⑤sizeof(a[1])4。a[1]是第二个int元素4 字节。⑥sizeof(a)4。a是整个数组的地址但地址本质还是指针4 字节。⑦sizeof(*a)16。*a里a是数组地址*解引用回去等价于a整个数组 16 字节。⑧sizeof(a1)4。a1是跳过整个数组之后的地址还是指针4 字节。⑨sizeof(a[0])4。a[0]是首元素地址指针4 字节。⑩sizeof(a[0]1)4。a[0]1是第二个元素地址指针4 字节。 规律只有sizeof(数组名)和sizeof(*数组名)这两种数组名/整体的写法才得到 16其余只要沾上、*、、[i]变成地址/元素的就按 4指针或按元素大小算。3.2 字符数组六段代码全解字符数组是sizeof/strlen的重灾区因为多了一个\0的问题。看六段代码每段都从sizeof和strlen两个角度问。代码 1sizeof——char arr[] {a,b,c,d,e,f};char arr[] {a,b,c,d,e,f}; // 6 个 char没有 \0共 6 字节 printf(%d\n, sizeof(arr)); // 6 printf(%d\n, sizeof(arr0)); // 4 printf(%d\n, sizeof(*arr)); // 1 printf(%d\n, sizeof(arr[1])); // 1 printf(%d\n, sizeof(arr)); // 4 printf(%d\n, sizeof(arr1)); // 4 printf(%d\n, sizeof(arr[0]1)); // 4sizeof(arr)6整个数组6 个char。sizeof(arr0)4arr0是首元素地址指针。sizeof(*arr)1*arr是首元素a1 个char。sizeof(arr[1])1一个char元素。sizeof(arr)4整个数组的地址指针。sizeof(arr1)4跳过整个数组后的地址指针。sizeof(arr[0]1)4第二个元素地址指针。代码 2strlen——同一个数组换strlen问char arr[] {a,b,c,d,e,f}; // 仍然没有 \0 printf(%d\n, strlen(arr)); // 随机值 printf(%d\n, strlen(arr0)); // 随机值 printf(%d\n, strlen(*arr)); // 报错非法访问 printf(%d\n, strlen(arr[1])); // 报错非法访问 printf(%d\n, strlen(arr)); // 随机值有类型警告 printf(%d\n, strlen(arr1)); // 随机值 - 6 printf(%d\n, strlen(arr[0]1)); // 随机值 - 1因为数组里没有\0所以strlen会一直向后越界找结果都是随机值strlen(arr)随机值从首地址开始越界找\0。strlen(arr0)随机值等价于arr。strlen(*arr)报错*arr是字符aASCII 97被当成地址97 去访问非法。strlen(arr[1])报错b98被当成地址非法。strlen(arr)随机值arr类型是char(*)[6]传给const char*会有警告但值还是数组首地址从那里开始越界找。strlen(arr1)随机值 - 6arr1跳过了整个 6 字节数组从数组末尾之后开始找。strlen(arr[0]1)随机值 - 1从第二个元素开始找比从头找少一个。⚠️ 注意strlen(*arr)和strlen(arr[1])这两个是非法访问/运行报错——strlen要的是地址这里却传了字符的值。代码 3sizeof——char arr[] abcdef;char arr[] abcdef; // 实际 7 字节a..f \0 printf(%d\n, sizeof(arr)); // 7 printf(%d\n, sizeof(arr0)); // 4 printf(%d\n, sizeof(*arr)); // 1 printf(%d\n, sizeof(arr[1])); // 1 printf(%d\n, sizeof(arr)); // 4 printf(%d\n, sizeof(arr1)); // 4 printf(%d\n, sizeof(arr[0]1)); // 4和代码 1 唯一的不同sizeof(arr)从 6 变成了7因为字符串abcdef会自动补一个\0。其余不变。代码 4strlen——char arr[] abcdef;char arr[] abcdef; // 有 \0 printf(%d\n, strlen(arr)); // 6 printf(%d\n, strlen(arr0)); // 6 printf(%d\n, strlen(*arr)); // 报错 printf(%d\n, strlen(arr[1])); // 报错 printf(%d\n, strlen(arr)); // 6有类型警告 printf(%d\n, strlen(arr1)); // 随机值 printf(%d\n, strlen(arr[0]1)); // 5这次有\0所以结果是确定的strlen(arr)6数到\0前正好 6 个字符。strlen(arr0)6等价于arr。strlen(*arr)/strlen(arr[1])报错字符值被当地址。strlen(arr)6有类型警告但值仍是首地址从那里数到\0是 6。strlen(arr1)随机值跳过了整个数组含\0从数组末尾之后继续找。strlen(arr[0]1)5从第二个字符b开始数到\0是 5。代码 5sizeof——char *p abcdef;注意是指针char *p abcdef; // p 是指针指向常量字符串的首地址 printf(%d\n, sizeof(p)); // 4 printf(%d\n, sizeof(p1)); // 4 printf(%d\n, sizeof(*p)); // 1 printf(%d\n, sizeof(p[0])); // 1 printf(%d\n, sizeof(p)); // 4 printf(%d\n, sizeof(p1)); // 4 printf(%d\n, sizeof(p[0]1)); // 4换成了指针p重点全变sizeof(p)4p是指针变量占 4 字节不管它指向哪。sizeof(p1)4p1还是指针。sizeof(*p)1*p是p指向的字符a1 字节。sizeof(p[0])1p[0]等价*(p0)即a1 字节。sizeof(p)4p是指针变量 p 自己的地址也是指针4 字节。sizeof(p1)4指针。sizeof(p[0]1)4指针。 关键区别char arr[] abcdef里sizeof(arr)是 7因为arr是数组char *p abcdef里sizeof(p)是 4因为p是指针。数组名和指针在sizeof下待遇完全不同。代码 6strlen——char *p abcdef;char *p abcdef; printf(%d\n, strlen(p)); // 6 printf(%d\n, strlen(p1)); // 5 printf(%d\n, strlen(*p)); // 报错 printf(%d\n, strlen(p[0])); // 报错 printf(%d\n, strlen(p)); // 随机值 printf(%d\n, strlen(p1)); // 随机值 printf(%d\n, strlen(p[0]1)); // 5strlen(p)6p指向字符串首地址数到\0是 6。strlen(p1)5从b开始数。strlen(*p)/strlen(p[0])报错a97被当地址。strlen(p)随机值⚠️ 这是最容易错的一个——p是指针变量 p 自己的地址不是字符串的地址从p变量所在的内存往后找\0结果随机。strlen(p1)随机值从p变量之后的地址开始找。strlen(p[0]1)5p[0]是字符串首地址1是第二个字符数到\0是 5。 区分两组地址p是字符串的地址strlen(p) 6p是指针变量自己的地址strlen(p)随机。p存的是字符串地址但p这个变量自己也占一块内存。3.3 二维数组int a[3][4] {0}; // 3 行 4 列共 12 个 int 48 字节 printf(%d\n, sizeof(a)); // ① printf(%d\n, sizeof(a[0][0])); // ② printf(%d\n, sizeof(a[0])); // ③ printf(%d\n, sizeof(a[0]1)); // ④ printf(%d\n, sizeof(*(a[0]1))); // ⑤ printf(%d\n, sizeof(a1)); // ⑥ printf(%d\n, sizeof(*(a1))); // ⑦ printf(%d\n, sizeof(a[0]1)); // ⑧ printf(%d\n, sizeof(*(a[0]1))); // ⑨ printf(%d\n, sizeof(*a)); // ⑩ printf(%d\n, sizeof(a[3])); // ⑪先记住二维数组a[3][4]的每一行a[0]、a[1]、a[2]都是一个一维数组每个一维数组有 4 个int占 16 字节。①sizeof(a)48整个二维数组12 个int。②sizeof(a[0][0])4一个int元素。③sizeof(a[0])16a[0]是第一行一维数组4 个int。④sizeof(a[0]1)4a[0]1是第一行第二个元素的地址指针。⑤sizeof(*(a[0]1))4解引用是第一行第二个元素一个int。⑥sizeof(a1)4a1是第二行的地址int(*)[4]指针。⑦sizeof(*(a1))16解引用是第二行这个一维数组16 字节。⑧sizeof(a[0]1)4a[0]是第一行地址1是第二行地址指针。⑨sizeof(*(a[0]1))16解引用是第二行一维数组16 字节。⑩sizeof(*a)16*a等价a[0]第一行一维数组。⑪sizeof(a[3])16⚠️ 有意思——a[3]其实越界了只有 a[0]~a[2]但sizeof是按类型算、不实际访问内存a[3]的类型还是int[4]所以仍是 16不报错。 二维数组的钥匙把一行当一个一维数组看。a[0]、*a、*(a1)这些解引用后是一行的写法sizeof都是 16而a1、a[0]1、a[0]1这些取地址/指针的写法sizeof都是 4。四、指针运算笔试题解析7 道经典题这一节是压轴大题每一道都藏着至少一个坑。继续默认 x86 环境。4.1 题目 1a1跳过了整个数组#include stdio.h int main() { int a[5] { 1, 2, 3, 4, 5 }; int *ptr (int *)(a 1); printf(%d,%d, *(a 1), *(ptr - 1)); return 0; }答案2,5推导*(a1)a[1]2这个简单。关键是ptra是整个数组的地址类型是int(*)[5]。a1的步长是整个数组20 字节所以它跳过整个数组指向a[4]之后的下一个位置。(int *)强转成int*后ptr指向数组末尾的下一个int。ptr - 1往回退 4 字节正好指回a[4]所以*(ptr-1)5。 坑点a1不是下一个元素而是下一个数组。数组名的步长是整个数组。4.2 题目 2结构体指针 1 的步长// X86 环境假设结构体大小是 20 字节 struct Test { int Num; char *pcName; short sDate; char cha[2]; short sBa[4]; } *p (struct Test*)0x100000; int main() { printf(%p\n, p 0x1); printf(%p\n, (unsigned long)p 0x1); printf(%p\n, (unsigned int*)p 0x1); return 0; }答案0x100014、0x100001、0x100004推导——三种加 1三种不同的步长p 0x1p是struct Test*步长是结构体大小 20 字节。0x100000 0x14(20)0x100014。(unsigned long)p 0x1先把p强转成无符号长整型普通整数1就是整数加 1不再按指针步长算。0x100000 10x100001。(unsigned int*)p 0x1转成unsigned int*步长是4 字节。0x100000 40x100004。 坑点指针 1 加的是步长取决于指针类型整数 1 加的是1。同一个地址转成不同的指针类型再 1结果天差地别。4.3 题目 3逗号表达式的陷阱#include stdio.h int main() { int a[3][2] { (0, 1), (2, 3), (4, 5) }; int *p; p a[0]; printf(%d, p[0]); return 0; }答案1推导花括号里(0,1)不是两个元素而是逗号表达式——逗号表达式从左到右依次求值结果是最后一个表达式的值。所以(0,1) 1(2,3) 3(4,5) 5数组实际被初始化为{1, 3, 5, 0, 0, 0}后面自动补 0p a[0]指向第一行首元素p[0]1。 坑点{ (0,1) }里的括号是逗号表达式不是元素分隔。逗号表达式取最后一个值。想表示两个元素得用{ {0,1}, {2,3}, {4,5} }。4.4 题目 4指针相减得到的是元素个数差// X86 环境 #include stdio.h int main() { int a[5][5]; int (*p)[4]; p a; printf(%p,%d\n, p[4][2] - a[4][2], p[4][2] - a[4][2]); return 0; }答案0xFFFFFFFC, -4第一个%p按十六进制地址打印-4的补码常见显示为FFFFFFFC推导——核心是p的行宽和a不一样p是int(*)[4]即p 认为一行只有 4 个int而a一行有5 个int。p a后类型不匹配但值一样都是首地址p按每行 4 个来解读这块内存。p[4][2]的偏移 4 * 4 218个inta[4][2]的偏移 4 * 5 222个int两个地址相减 18 - 22-4表示p[4][2]比a[4][2]靠前 4 个int。 坑点指针相减得到的是两者之间差几个元素按int计不是字节差。这里p的行宽4和a的行宽5错位导致同一片内存解读出不同的元素下标。4.5 题目 5二维数组的aa1和*(aa1)#include stdio.h int main() { int aa[2][5] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int *ptr1 (int *)(aa 1); int *ptr2 (int *)(*(aa 1)); printf(%d,%d, *(ptr1 - 1), *(ptr2 - 1)); return 0; }答案10,5推导aa是整个二维数组的地址aa1跳过整个数组2 行 × 5 列 40 字节指向数组末尾之后。(int*)ptr1指向末尾的下一个intptr1 - 1退 4 字节指回最后一个元素aa[1][4]10。*(aa1)aa是首行地址aa1是第二行地址*(aa1)aa[1]第二行。强转int*后ptr2指向第二行首元素aa[1][0] 6。ptr2 - 1退 4 字节指回aa[0][4]5。 和题目 1 一个套路aa1跳整个数组而*(aa1)是第二行等价aa[1]。两题合起来把与*、行与整体的步长彻底练透。IMG44.6 题目 6二级指针 1#include stdio.h int main() { char *a[] { work, at, alibaba }; char **pa a; pa; printf(%s\n, *pa); return 0; }答案at推导a是指针数组a[0]、a[1]、a[2]分别存work、at、alibaba的首地址。pa apa指向a[0]。pa后pa指向a[1]。*paa[1]at的首地址printf(%s, *pa)打印at。 二级指针pa指向指针数组pa让pa指向下一个指针a[1]再*pa解引用拿到那个字符串。4.7 题目 7压轴三级指针char***cpp阿里/腾讯经典题#include stdio.h int main() { char *c[] {ENTER, NEW, POINT, FIRST}; char **cp[] {c3, c2, c1, c}; char ***cpp cp; printf(%s\n, **cpp); printf(%s\n, *--*cpp3); printf(%s\n, *cpp[-2]3); printf(%s\n, cpp[-1][-1]1); return 0; }答案按顺序四行POINT、ER、ST、EW这道题几乎把所有指针运算都串起来了。先画出初始内存布局配合图 5c是字符串指针数组c[0]→ENTER、c[1]→NEW、c[2]→POINT、c[3]→FIRSTcp是指针数组存的都是c里元素的地址cp[0]c3、cp[1]c2、cp[2]c1、cp[3]ccpp cpcpp指向cp[0]。第一句**cppcppcpp自增从指向cp[0]变成指向cp[1]*cppcp[1]c2**cpp*(c2)c[2]POINT。输出POINT。此时cpp已指向cp[1]cpp变了要记住。第二句*--*cpp3cppcpp从cp[1]指向cp[2]*cppcp[2]c1--*cpp即--(c1)先自减变成c指向c[0]。⚠️ 注意这同时把cp[2]的值从c1改成了c*(c)c[0]ENTER3字符串ENTER首地址往后偏移 3 个字符落到ER。输出ER。此时cpp指向cp[2]且cp[2]已被改成c。第三句*cpp[-2]3cpp[-2]*(cpp-2)cp[0]c3⚠️ 这里cpp本身没变只是往前两个读cp[0]*cpp[-2]*(c3)c[3]FIRST3FIRST偏移 3 个字符落到ST。输出ST。第四句cpp[-1][-1]1cpp[-1]*(cpp-1)cp[1]c2cpp[-1][-1]*(cp[1] - 1)*(c2-1)*(c1)c[1]NEW1NEW偏移 1 个字符落到EW。输出EW。 这道题的三把钥匙①cpp会永久改变cpp指向后三句都建立在前一句改过的cpp上②--*cpp会改cp[2]本身的值副作用③cpp[-1]、cpp[-2]只是相对读取不改变cpp。做题时画一张图每执行一句就更新一次箭头答案就一目了然。IMG5总结最后把这一讲的骨架拎出来sizeof vs strlensizeof是操作符算内存大小不关心内容strlen是库函数算字符串长度\0之前字符个数没\0会越界。数组名三大规则sizeof(数组名)和数组名代表整个数组其余一律是首元素地址。一维/字符/二维数组题只要分得清整个数组 / 一行 / 一个元素 / 一个地址这四种身份sizeof结果就分别是整体大小 / 一行大小 / 元素大小 / 4指针。char arr[]vschar *p数组名在sizeof下算整个数组指针在sizeof下算指针本身p是字符串地址p是指针变量自己的地址。指针运算题a1跳整个数组、结构体指针1跳结构体大小、整数1就是加 1指针相减得元素个数差逗号表达式取最后一个值。三级指针压轴题cpp会改变指向、--*cpp有副作用、cpp[-1]只是相对读取——画图一步步追。一句话收尾指针的终极考点就两条——量内存还是数字符sizeof vs strlen整个数组还是首元素地址数组名规则。看懂这两条所有sizeof/strlen/指针运算的笔试题都是纸老虎。拓展学习sizeof在 64 位下的差异指针从 4 变 8本讲所有指针相关 4的答案都要改成 8其余元素、数组整体不变。内存函数memcpy、memset、memmove用void*配合sizeof对任意内存按字节操作。复杂声明的右左法则遇到char (*(*x[3])())[5]这类用从变量名出发、先右后左的方法读。上一讲深入理解指针(4)回调函数、qsort、void* 指针、qsort 模拟实现