news 2026/9/28 8:03:46

深入理解指针6 - sizeof和strlen、数组和指针练习、指针运算

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解指针6 - sizeof和strlen、数组和指针练习、指针运算

目录

1.sizeof和strlen的对比

2.数组和指针题练习

3.指针运算


1.sizeof和strlen的对比

1.1区分

sizeofstrlen
本质

运算符

库函数

头文件不需要头文件需要<string.h>
参数可以是类型,变量,表达式,数组名等

必须是char*或const char* 类型的。

(即参数必须是首字符指针)

计算内容

1.占用的内存字节数,包括任何结束符\0或填充字节;

2.不在乎内存中存放的是什么数据;

1.字符串长度,即从开头到第一个\0的字符个数(不包含\0)

2.如果内存中没有 \0 ,就会持续往后找,可能会越界

结果类型size_t (无符号整数),单位是字节size_t

2.数组和指针题练习

回顾数组名的意义:

1. sizeof(数组名),这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩。

2. &数组名,这⾥的数组名表示整个数组,取出的是整个数组的地址。

3. 除此之外所有的数组名都表示⾸元素的地址

1 -一维数组

sizeof 与 一维数组

int main() { int a[] = { 1,2,3,4 }; printf("%zu\n", sizeof(a));//16,数组名单独放sizeof()里,计算的是整个数组的大小 printf("%zu\n", sizeof(a + 0));//4/8,a+0是首元素的地址,地址就是4/8 printf("%zu\n", sizeof(*a));//4, *a == a[0] printf("%zu\n", sizeof(a + 1));//4/8, a + 1 --> &a[1] printf("%zu\n", sizeof(a[1]));//4 printf("%zu\n", sizeof(&a));//4/8, &a是整个数组的地址,依然是地址 printf("%zu\n", sizeof(*&a));//16 //&a -- int (*)[4] //*&a -- a //sizeof(*&a) == sizeof(a) printf("%zu\n", sizeof(&a + 1));//4/8 &a + 1还是地址 printf("%zu\n", sizeof(&a[0]));//4/8 printf("%zu\n", sizeof(&a[0] + 1));//4/8 return 0; }

总结:

  • 数组名只在两种情况下不退化为指针:作为sizeof的操作数,或者作为&的操作数(如&a);

  • 一旦参与运算(如a + 0,a + 1),数组名就会退化为指向首元素的指针;

  • sizeof作用于指针时,结果是指针变量本身的大小(32 位 4 字节,64 位 8 字节),与它指向的空间大小无关;

2 - 字符数组

sizeof 与 字符数组

int main() { char arr1[] = { 'a','b','c','d','e','f' };//用字符列表初始化 printf("%zu\n", sizeof(arr));//6 printf("%zu\n", sizeof(arr + 0));//4/8, arr + 0 == &arr[0] printf("%zu\n", sizeof(*arr));//1, *arr == arr[0] printf("%zu\n", sizeof(arr[1]));//1 printf("%zu\n", sizeof(&arr));//4/8 -- char(*)[6] printf("%zu\n", sizeof(&arr + 1));//4/8-- char(*)[6] printf("%zu\n", sizeof(&arr[0] + 1));//4/8 return 0; }
int main() { char arr2[] = "abcdef";////用字符串初始化 printf("%zu\n", sizeof(arr));//7 printf("%zu\n", sizeof(arr + 0));//4/8,地址 printf("%zu\n", sizeof(*arr));//1, arr[0] == *arr printf("%zu\n", sizeof(arr[1]));//1 printf("%zu\n", sizeof(&arr));//4/8 printf("%zu\n", sizeof(&arr + 1));//4/8 printf("%zu\n", sizeof(&arr[0] + 1));//4/8 return 0; }

注意:第一段代码的数组是用字符初始化的

第二段代码的数组是用字符串初始化的

strlen 与 字符数组

int main() { char arr[] = { 'a','b','c','d','e','f' };//用字符列表初始化 printf("%zu\n", strlen(arr));//未知数,字符数组没有 \0,strlen 会一直越界读取 printf("%zu\n", strlen(arr + 0));//未知数 printf("%zu\n", strlen(*arr));//*arr == arr[0] == 'a' == 97, 程序会崩溃(strlen的参数必须是地址,这里会把97理解为地址访问) printf("%zu\n", strlen(arr[1]));//'b'-98,程序会崩溃 printf("%zu\n", strlen(&arr));//未知数 (没有 '\0',越界读取,结果未知) //char(*)[6]-> const char* printf("%zu\n", strlen(&arr + 1));//未知数(指向 arr 之后的地址。这个地址仍在数组之外,从此处开始找 '\0' 同样是越界读取,结果未定义) printf("%zu\n", strlen(&arr[0] + 1));//未知数 (从 'b' 开始找 '\0',依然没有终止符,越界读取,结果未定义) return 0; }
int main() { char arr[] = "abcdef";//用字符串初始化 printf("%zu\n", strlen(arr));//6 printf("%zu\n", strlen(arr + 0));//6 //printf("%zu\n", strlen(*arr));//程序会崩溃 //printf("%zu\n", strlen(arr[1]));//程序会崩溃 printf("%zu\n", strlen(&arr));//6 printf("%zu\n", strlen(&arr + 1));//未知数 printf("%zu\n", strlen(&arr[0] + 1));//5 return 0; }

总结:

  • 没有'\0'的字符数组绝不能直接传给strlen,会产生越界读取(未定义行为),得到的结果无意义且可能影响程序稳定性;

  • 不能把单个字符(整数)当作地址传给strlen,这几乎必然导致程序崩溃;

sizeof 与 指针

int main() { char* p = "abcdef"; printf("%zu\n", sizeof(p));//4/8 printf("%zu\n", sizeof(p + 1));//4/8, p+1是b的地址 printf("%zu\n", sizeof(*p));//1, *p == 'a' printf("%zu\n", sizeof(p[0]));//1 //p[0] == *(p+0) == 'a' printf("%zu\n", sizeof(&p));//4/8 printf("%zu\n", sizeof(&p + 1));//4/8 printf("%zu\n", sizeof(&p[0] + 1));//4/8, 'b'的地址 return 0; }

注意:p放的是字符串''abcdef''的首地址

strlen 与 指针

int main() { char* p = "abcdef"; printf("%zu\n", strlen(p));//6 printf("%zu\n", strlen(p + 1));//5 printf("%zu\n", strlen(*p));//程序崩溃 printf("%zu\n", strlen(p[0]));//程序崩溃 printf("%zu\n", strlen(&p));//未知数, p是指针变量,p 是 char* 类型的指针变量,&p 的类型是 char**(指向指针的指针)。&p放的是p的地址,不是a的地址 printf("%zu\n", strlen(&p + 1));//未知数, 指向 p 变量之后的内存区域 printf("%zu\n", strlen(&p[0] + 1));//5 &p[0] + 1 -> 'b'的地址 return 0; }

3 - 二维数组

sizeof 和 二维数组

​ int main() { int a[3][4] = { 0 }; printf("%zu\n", sizeof(a));//3*4*4=48 printf("%zu\n", sizeof(a[0][0]));//4 printf("%zu\n", sizeof(a[0]));//16 ,a[0]是二维数组的首元素,是一个一维数组,所以a[0]是第一行的数组名,计算的是第一行的大小,单位是字节 printf("%zu\n", sizeof(a[0] + 1));//4/8, a[0] + 1-->&a[0][1],某个元素的地址 printf("%zu\n", sizeof(a + 1));//4/8, a+1是第二行的地址 //a[0]+1:a[0]是一维数组,类型是 int* ,+1跳过一个 int元素,所以指向 a[0][1] / a[0]在这里退化为首元素地址,再+1,所以代表的就是a[0][1]的地址 //a+1: a 是二维数组,这里退化为首元素的的地址,即第一行的地址,类型是 int(*)[4] ,+1跳过一行 printf("%zu\n", sizeof(*(a[0] + 1)));//4, *(a[0] + 1))->a[0][1] //a-> int(*)[4] printf("%zu\n", sizeof(*(a + 1)));//16 //*(a+1)==a[1] printf("%zu\n", sizeof(&a[0] + 1));//4/8, &a[0] + 1是第二行的地址;&a[0]是取出了整个第一行的地址,+1就是指向第二行 printf("%zu\n", sizeof(*(&a[0] + 1)));//16 printf("%zu\n", sizeof(*a));//16 printf("%zu\n", sizeof(a[3]));//16, a[3] 看起来像是越界访问,但实际上 sizeof 不会对表达式进行实际求值),它只关心表达式的类型 //a[3] --> a[0] //int[4] return 0; } ​

printf("%zu\n", sizeof(a));//3*4*4=48 printf("%zu\n", sizeof(a[0]));//16

//第一句 a 是数组名,计算的是整个数组的大小

//第二句 a[0] 是数组名,它是二维数组的首元素,是一个数组,所以 a[0] 是第一行的数组名,计算的是第一行的大小,单位是字节


printf("%zu\n", sizeof(a[0] + 1));//4/8 printf("%zu\n", sizeof(a + 1));//4/8

//第一句 a[0] + 1 ,a[0] 是一维数组,这里退化为首元素地址,类型是 int* , +1跳过一个int 元素,所以指向 a[0][1]

//第二句 a+1 ,a 是二维数组,这里退化为首元素地址,也就是第一行的地址,类型是

int(*)[4] ,+1跳过一行,所以 a+1 是第二行的地址


printf("%zu\n", sizeof(&a[0] + 1));//4/8

&a[0] 取出的是整个第一行的地址,+1就是指向第二行


printf("%zu\n", sizeof(a[3]));//16

a[3]看起来像越界访问,但实际上 sizeof 不会对表达式进行求值,它只关心表达式的类型;和a[0]、a[1]是一样的


3.指针运算

例题1

int main() { int a[5] = { 1, 2, 3, 4, 5 }; int* ptr = (int*)(&a + 1); printf("%d,%d", *(a + 1), *(ptr - 1));//2,5 return 0; }

解析:

如图,最终输出 2 和 5

例题2

下面这段代码,在x86环境下,假设结构体的大小是20个字节,程序的输出结果是啥?

struct Test { int Num; char* pcName; short sDate; char cha[2]; short sBa[4]; }*p = (struct Test*)0x100000; int main() { printf("r1==%p\n", p + 0x1);//00100014 printf("r2==%p\n", (unsigned long)p + 0x1);//00100000 printf("r3==%p\n", (unsigned int*)p + 0x1);//00100004 return 0; }

解析:

0x1就是1

1 - p + 0x1

  • p是个指针变量,它的类型是 struct Test*,指向的是struct Test类型的的数据
  • 指针+1会增加 sizeof(struct Test)个字节,也就是20个字节
  • 所以 p+1 就是加了20(十六进制是 0x14)个字节 , r == 0x100014 (以%p打印的时候,可能会打印00100014)

2 - (unsigned long)p + 0x1

  • p被强转为无符号长整型,即 p 变成了整数值 0x100000
  • 整数加 1 就是普通的加法→ 0x100000+1=0x100000

  • 输出 r2 == 00100000

3 - (unsigned long*)p + 0x1

  • p被强转为 unsigned long* 类型的指针
  • +1 跳过4个字节
  • 所以输出 r3 == 00100004

例题3

int main() { int a[3][2] = { (0, 1), (2, 3), (4, 5) }; int* p; p = a[0]; printf("%d", p[0]);//1 return 0; }

解析:

注意:(0, 1), (2, 3), (4, 5)是逗号表达式,并不是在按行初始化

  • 逗号操作符会依次计算左右操作数,并返回右边操作数的值。所以:

(0, 1)→ 值为1

(2, 3)→ 值为3

(4, 5)→ 值为5

  • 于是这行初始化等价于 =>int a[3][2] = { 1 , 3 , 5 };//其他元素都是0
  • 所以 p[0] == *(p+0) == a[0][0] == 1

例题4

假设环境是x86环境,程序输出结果是啥?

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]);//FFFFFFFC,-4 return 0; }

解析:

  • int a[5][5];// a 的实际行宽为 5 个 int(20 字节)int(*p)[4];// p 是指向 int[4] 的指针,行宽为 4 个 int(16 字节)p = a;// p 指向 a 的起始地址,但类型不匹配,按 p 的类型解析

如上图所示,

  • 指针相减,得到的是地址之间的元素个数,&p[4][2] - &a[4][2] == -4,但是%p将差值-4作为地址输出,通常会输出其补码形式FFFFFFFC;
  • %d正常输出‑4

例题5

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));//10,5 return 0; }

解析:

如图,最终输出10,5

例题6

int main() { char* a[] = { "work","at","alibaba" }; char** pa = a; pa++; printf("%s\n", *pa);//at return 0; }

解析:

如图,

  • *pa 得到 a[1],也就是指向 "at" 的指针;

  • %s 打印字符串 at;

例题7

int main() { char* c[] = { "enter","new","point","first" }; char** cp[] = { c + 3,c + 2,c + 1,c }; char*** cpp = cp; printf("r1 == %s\n", **++cpp); printf("r2 == %s\n", *-- * ++cpp + 3); printf("r3 == %s\n", *cpp[-2] + 3); printf("r4 == %s\n", cpp[-1][-1] + 1); return 0; }

解析:

注意操作符的优先级:最高 [ ] , 较高 ++和 -- , 其次 * , 最后 +和 - ;

[ ] :E1[ E2] == *( E1+E2); 如,cp[ 2 ] == *(cp + 2) ;

[ ]就相当于解引用;

printf("r1 == %s\n", **++cpp); printf("r2 == %s\n", *-- * ++cpp + 3); printf("r3 == %s\n", *cpp[-2] + 3); printf("r4 == %s\n", cpp[-1][-1] + 1);
  • c:四个字符串指针;

  • cp:元素依次指向 c[3], c[2], c[1], c[0];

  • cpp 初始指向 cp[0];


1 - **++cpp

printf("r1 == %s\n", **++cpp);

如图,(蓝色的线代表cpp一开始存的地址,绿色的线代表**++cpp的步骤)

  • ++cpp:cpp+1指向cp[1]
  • *++cpp:解引用一次,*++cpp == cp[1],即c+2(指向c[2])
  • **++cpp:再解引用,**++cpp == *cp[1] == p的地址,%s 打印字符串
  • 输出point

(注:++cpp后,改变了cpp的值,cpp不再指向cp,而是指向cp[1])


2 - *--*++cpp+3

printf("r2 == %s\n", *-- * ++cpp + 3);

按优先级结合:*( --(*(++cpp)))+3

  • ++cpp:cpp 指向 cp[2];
  • *++cpp→ cp[2],即 c+1(指向 c[1]);
  • -- * ++cpp:对 cp[2] 做 --,变为 c(指向 c[0]);(即cp[2] 被改为 c)
  • *(-- * ++cpp)→ *c → c[0] → "enter"。
  • 最后 +3:指针偏移 3,得到 "enter"[3] → "er"。
  • 输出:er

(注:此时 cpp 指向 cp[2],且cp[2] 已修改为 c)


3 - *cpp[-2]+3

printf("r3 == %s\n", *cpp[-2] + 3);

(注:这句代码中,cpp的值没有被修改,所以是从cp[2]开始的)

  • cpp[-2]即 *(cpp - 2)→ cp[0](仍是 c+3,未被修改);
  • * cpp[-2]→ *cp[0] → c[3] → "first";
  • +3→ "first"[3] → "st";
  • 输出:st (cpp 不变,仍指向 cp[2]);

4 - cpp[-1][-1]+1

printf("r4 == %s\n", cpp[-1][-1] + 1);

(注:cpp不变,扔指向cp[2])

  • cpp[-1]→ *(cpp-1) → cp[1] ,指向 c[2];
  • cpp[-1][-1]→ *( *(cpp-1) -1)→ c[1] → "new";//[ ]就相当于解引用;

//cpp[-1][-1]也可以写成 *(c+2 - 1) → c[1] → "new";

  • +1→ "new"[1] → "ew";
  • 输出:ew

总结:

  1. 数组名退化为指针:c 退化为 char**,cp 退化为 char***
  2. E1[E2] = *(E1+E2):所有下标都可转化为指针加减,负数合法
  3. 优先级:后缀 [ ] 最高,前缀 ++/--/ * 次之,算术 + 最后
  4. 自增自减会修改原值:第二步 -- * ++cpp 永久改变了 cp[2]
  5. 字符串偏移:对字符指针加 n,就是要偏移 n 个字符,从那个字符开始算起

所以复盘时要注意:

  1. 画清楚初始指针关系图
  2. 每一步跟踪 cpp 的位置变化
  3. 用正确的方法分析优先级
  4. 注意哪些操作造成了副作用(自增、自减),它们会影响后续语句
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 8:03:24

MCU+FPGA上电启动随机故障排查:电源时序与配置握手全解析

上电的一瞬间&#xff0c;板上5V、3.3V、1.2V的电源指示灯全亮了&#xff0c;MCU的调试串口也正常打印了启动日志&#xff0c;一切看起来都很正常&#xff0c;唯独FPGA没醒过来&#xff1a;DONE指示灯不亮&#xff0c;业务IO全部处于高阻&#xff0c;整块板子像被抽走了主心骨。…

作者头像 李华
网站建设 2026/9/28 8:03:23

SpringBoot+Vue3党员教育管理系统实战:从数据库设计到部署避坑

前阵子帮朋友检查一套“党员教育和管理系统”的完整源码&#xff0c;项目技术栈是Java SpringBoot Vue3 MyBatis MySQL&#xff0c;前后端分离&#xff0c;典型的后台管理型系统。说实话&#xff0c;这类系统从业务上看并不复杂——党员信息管理、学习资料发布、在线学习记录…

作者头像 李华
网站建设 2026/9/28 8:03:12

深度学习环境排查:CUDA、PyTorch版本查询与匹配实战指南

注意&#xff1a;以下内容未检测到任何违反安全规范的内容&#xff0c;请放心阅读。1. 版本查询这件小事&#xff0c;为什么值得认真对待先讲个真实经历。以前接手过一台别人配好的深度学习工作站&#xff0c;上面跑着一个训练脚本&#xff0c;动不动就报CUDA error: no kernel…

作者头像 李华
网站建设 2026/9/28 8:03:00

0.152mm厚黄色K10矽胶布实力生产厂家推荐,资质齐全不踩坑

如何快速辨别靠谱的0.152mm黄色K10矽胶布供应商?行业资深玩家都在看的避坑指南在电子元器件热管理与绝缘防护的赛道里&#xff0c;0.152mm黄色K10矽胶布作为高功率器件散热的核心材料&#xff0c;近年来需求持续攀升。不少商家在选型时陷入两难&#xff1a;既要保证导热、绝缘…

作者头像 李华
网站建设 2026/9/28 8:02:45

MiMo接管Blender:AI原生建模工作流重构

1. 项目概述&#xff1a;这不是一个“插件”&#xff0c;而是一次Blender工作流的底层重写“让 MiMo 亲手给你捏头金牛”——这个标题乍看像营销话术&#xff0c;但拆开来看&#xff0c;每个词都踩在当前3D内容创作最敏感的神经上。“MiMo”不是泛指多输入多输出架构&#xff0…

作者头像 李华
网站建设 2026/9/28 8:02:22

电梯监控电动车识别实战:YOLO目标检测与部署避坑指南

简介&#xff1a;面向人工智能与计算机视觉方向的学习者及毕业设计开发者&#xff0c;这一压缩包围绕电梯监控视角下的电动车与自行车识别任务&#xff0c;提供了一套可运行、可复现的AI视觉项目方案。资源共134个文件&#xff0c;以YAML配置、Python脚本、Jupyter教程、Docker…

作者头像 李华