目录
1.sizeof和strlen的对比
2.数组和指针题练习
3.指针运算
1.sizeof和strlen的对比
1.1区分
| sizeof | strlen | |
| 本质 | 运算符 | 库函数 |
| 头文件 | 不需要头文件 | 需要<string.h> |
| 参数 | 可以是类型,变量,表达式,数组名等 | 必须是char*或const char* 类型的。 (即参数必须是首字符指针) |
| 计算内容 | 1.占用的内存字节数,包括任何结束符 2.不在乎内存中存放的是什么数据; | 1.字符串长度,即从开头到第一个 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]));//16a[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
总结:
- 数组名退化为指针:c 退化为 char**,cp 退化为 char***
- E1[E2] = *(E1+E2):所有下标都可转化为指针加减,负数合法
- 优先级:后缀 [ ] 最高,前缀 ++/--/ * 次之,算术 + 最后
- 自增自减会修改原值:第二步 -- * ++cpp 永久改变了 cp[2]
- 字符串偏移:对字符指针加 n,就是要偏移 n 个字符,从那个字符开始算起
所以复盘时要注意:
- 画清楚初始指针关系图
- 每一步跟踪 cpp 的位置变化
- 用正确的方法分析优先级
- 注意哪些操作造成了副作用(自增、自减),它们会影响后续语句