C语言指针深入详解(六):sizeof和strlen的对比,【题解】数组和指针笔试题解析、指针运算笔试题解析
目录
一、sizeof和strlen的对比
(一)sizeof
(二)strlen
(三)sizeof和strlen的对比
二、数组和指针笔试题解析
(一)一维数组
(二)字符数组
(三) 二维数组
三、指针运算笔试题解析
结论
🔥个人主页:艾莉丝努力练剑
🍓专栏传送门:《C语言》
🍉学习方向:C/C++方向
⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平
前言:前面几篇文章介绍了c语言的一些知识,包括循环、数组、函数、VS实用调试技巧、函数递归、操作符等,在这篇文章中,我将继续介绍指针剩余的一些重要知识点!鉴于指针的内容较多,博主将会分为六篇博客介绍,这是第六篇!是的,指针部分到这里就更完啦!对指针感兴趣的友友们可以在评论区一起交流学习!
一、sizeof和strlen的对比
(一)sizeof
学习操作符的时候,我们学习了 sizeof , sizeof 计算变量所占内存空间大小的,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据。
比如:
#inculde <stdio.h>int main()
{int a = 10;printf("%d\n", sizeof(a));printf("%d\n", sizeof a);printf("%d\n", sizeof(int));return 0;
}
(二)strlen
strlen 是C语言库函数,功能是求字符串长度。函数原型如下:
size_t strlen ( const char * str );
统计的是从strlen函数的参数str中这个地址开始向后, \0 之前字符串中字符的个数。
strlen函数会一直向后找 \0 字符,直到找到为止,所以可能存在越界查找。
#include <stdio.h>int main()
{char arr1[3] = {'a', 'b', 'c'};char arr2[] = "abc";printf("%d\n", strlen(arr1));printf("%d\n", strlen(arr2));printf("%d\n", sizeof(arr1));printf("%d\n", sizeof(arr2));return 0;
}
(三)sizeof和strlen的对比
二、数组和指针笔试题解析
(一)一维数组
友友们可以尝试算一下下面的结果:
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
(二)字符数组
字符数组这儿有六个代码,和一维数组那儿一样,友友们可以尝试算一下结果:
代码(1):
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
下面是结果,大家算对了吗?
对照注释,大家再理解一下:
#include<stdio.h>
int main()
{char arr[] = { 'a','b','c','d','e','f' };printf("%zu\n", sizeof(arr));//6//arr是数组名,且放在sizeof中,所以是算整个数组大小,且是char,所以为6printf("%zu\n", sizeof(arr + 0));// 4/8//数组名arr在这里是数组首元素地址,是地址就是4/8printf("%zu\n", sizeof(*arr));// 1//数组名arr在这里是数组首元素地址,所以*arr就是数组首元素,char类型,为1printf("%zu\n", sizeof(arr[1]));// 1//arr[1]是数组第二个元素,char类型.为1printf("%zu\n", sizeof(&arr));// 4/8//&arr是数组地址,是地址就是4/8printf("%zu\n", sizeof(&arr + 1));// 4/8//&arr是数组地址。&arr+1是跳过一整个数组的地址,是地址就是4/8printf("%zu\n", sizeof(&arr[0] + 1));// 4/8//&arr[0]+1是数组第二个元素的地址,是地址就是4/8
}
代码(2):
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
#include<stdio.h>
#include<string.h>
int main()
{char arr[] = { 'a','b','c','d','e','f' };printf("%zu\n", strlen(arr));//随机值//数组名arr在这里是数组首元素地址,从这开始往后数,直到\0为止,但是找不到\0//所以会越界,产生随机值printf("%zu\n", strlen(arr + 0));//随机值//和上一句代码理由差不多,因为arr+0还是数组首元素地址//printf("%zu\n", strlen(*arr));//err,崩溃//arr在这里是数组首元素地址,*arr是数组首元素,也就是a,a-97//这里给到strlen的不是地址,[strlen(const char* p)]类型不符合,所以会编译错误//printf("%zu\n", strlen(arr[1]));//err,崩溃//arr[1]是数组第二个元素,后续缘由和上面相同printf("%zu\n", strlen(&arr));//随机值//&arr是数组的地址,但还是从首元素开始的,找\0,但是找不到,会越界printf("%zu\n", strlen(&arr + 1));//随机值//&arr是数组的地址,&arr+1是跳过一整个数组后的地址,但还是找不到\0//这里也会是随机值,不过和前面的不同,和第一,第二个的差6printf("%zu\n", strlen(&arr[0] + 1));//随机值//&arr[0]+1是数组第二个元素的地址,从这往后面找,但还是和前面情况相同,所以是随机值,但也是不同的//和第一,第二个的差1
}
代码(3):
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
#include<stdio.h>
int main()
{char arr[] = "abcdef";//字符串里隐藏了个\0,所以其实是7个元素printf("%zu\n", sizeof(arr));//7//数组名arr直接放在sizeof中,算的是整个数组的大小,所以是7printf("%zu\n", sizeof(arr + 0));// 4/8//这里的数组名arr是数组的首元素地址,arr+0还是数组首元素的地址//是地址就是4/8printf("%zu\n", sizeof(*arr));//1//这里的数组名arr是数组的首元素地址,*arr是数组首元素,char类型,所以是1printf("%zu\n", sizeof(arr[1]));//1//arr[1]是数组第二个元素,char类型,所以是1printf("%zu\n", sizeof(&arr));// 4/8//&arr是数组的地址,是地址就是4/8printf("%zu\n", sizeof(&arr + 1));// 4/8//&arr是数组的地址,&arr+1是跳过一整个数组后的地址,是地址就是4/8printf("%zu\n", sizeof(&arr[0] + 1));// 4/8//&arr[0]+1是第二个元素的地址,是地址就是4/8
}
代码(4):
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
#include<stdio.h>
#include<string.h>
int main()
{char arr[] = "abcdef";//字符串里隐藏了个\0,所以其实是7个元素printf("%zu\n", strlen(arr));//6//这里的数组名arr是数组首元素的地址,从这开始往后找,直到\0;printf("%zu\n", strlen(arr + 0));//6//这里的数组名arr是数组首元素地址,arr+0还是,所以和上面的缘由一样//printf("%zu\n", strlen(*arr));//err,崩溃//*arr是数组首元素,这里给到strlen的不是地址// [strlen(const char* p)]类型不符合,所以会编译错误//printf("%zu\n", strlen(arr[1]));//err,崩溃//arr[1]是数组的第二个元素,理由和上面相同printf("%zu\n", strlen(&arr));//6//&arr是数组的地址,但还是从首元素开始的,直到\0printf("%zu\n", strlen(&arr + 1));//随机值//&arr是数组的地址,& arr + 1是跳过一整个数组后的地址,但就找不到\0了//会越界,产生随机值printf("%zu\n", strlen(&arr[0] + 1));//5//&arr[0]+1是第二个数组的地址,从这里开始,直到\0
}
代码(5):
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
#include<stdio.h>
int main()
{char* p = "abcdef";//这里p存放的是首字符a的地址printf("%zu\n", sizeof(p));// 4/8//p存放的a的地址,是地址就是4/8printf("%zu\n", sizeof(p + 1));// 4/8//p+1(指针+1)还是地址,是b的地址,是地址就是4/8printf("%zu\n", sizeof(*p));// 1//p里存放的是a的地址,*p就是a,char类型,所以是1printf("%zu\n", sizeof(p[0]));// 1//p[0]=*(p+0)=*p=a,char类型,所以是1printf("%zu\n", sizeof(&p));// 4/8//&p是指针变量p的地址,是地址就是4/8printf("%zu\n", sizeof(&p + 1));//4/8//&p+1本质上也还是个地址,是地址就是4/8printf("%zu\n", sizeof(&p[0] + 1));// 4/8//&p[0]+1是第二个元素也就是b的地址,是地址就是4/8
}
代码(6):
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
#include<stdio.h>
#include<string.h>
int main()
{char* p = "abcdef";//这里p存放的是首字符a的地址printf("%zu\n", strlen(p));//6//p里存放的是首字符a的地址,从这里开始找到\0为止printf("%zu\n", strlen(p + 1));//5//p+1是第二个字符b的地址,从这里开始也是找到\0为止//printf("%zu\n", strlen(*p));//err,崩溃// *p=a,这不是一个地址,和sizeof的参数类型不符合//printf("%zu\n", strlen(p[0]));//err,崩溃//p[0]=*(p+0)=*p=a,所以和上面一样printf("%zu\n", strlen(&p));//随机值//&p是指针变量p的地址,在这是找不到\0的,所以会是随机值printf("%zu\n", strlen(&p + 1));//随机值//&p+1是&p向后访问之后的地址,也是找不到\0的,但是随机值和上面不同printf("%zu\n", strlen(&p[0] + 1));//5//&p[0]+1就是从第二个元素也就是b的地址,从这里开始往后直到\0}
(三) 二维数组
友友们算一算下面打印的结果:
int a[3][4] = {0};
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(a+1));
printf("%d\n",sizeof(*(a+1)));
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]));
#include<stdio.h>
int main()
{int a[3][4] = { 0 };printf("%zu\n", sizeof(a));// 48//a是二维数组数组名,直接放在了sizeof中,计算的整个二维数组大小,12*4printf("%zu\n", sizeof(a[0][0]));//4//二维数组第一行第一个元素,int类型的printf("%zu\n", sizeof(a[0]));//16//a[0]是二维数组第一行一维数组的数组名,数组名直接放在sizeof中//算的是这一行一维数组的大小,4*4=16printf("%zu\n", sizeof(a[0] + 1));// 4/8//a[0]是二维数组第一行一维数组的数组名,在这里就是一维数组首元素的地址//a[0]+1是第一行一维数组第二个元素的地址,是地址就是4/8printf("%zu\n", sizeof(*(a[0] + 1)));// 4//a[0]+1是第一行一维数组第二个元素的地址,*(a[0] + 1)就是第二个元素,int类型printf("%zu\n", sizeof(a + 1));//4/8//a在这里是二维数组的首元素的地址,也就是第一行一维数组的地址,a+1则是第二行一维数组的地址//是地址就是4/8printf("%zu\n", sizeof(*(a + 1)));// 16//a + 1是第二行一维数组的地址,*(a + 1)则是第二行一维数组中的元素,4个,4*4=16printf("%zu\n", sizeof(&a[0] + 1));// 4/8//&a[0]是第一行一维数组的地址,&a[0]+1是跳过一整个数组之后的地址//其实也就是第二行一维数组的地址,是地址就是4/8printf("%zu\n", sizeof(*(&a[0] + 1)));//16//&a[0] + 1是第二行一维数组的地址,所以*(&a[0] + 1)是第二行所有元素,一共4个,4*4=16printf("%zu\n", sizeof(*a));//16//a是二维数组数组名,在这里就是二维数组的首元素的地址,也就是第一行一维数组的地址//*a则是第一行一维数组的所有元素,4个,4*4=16printf("%zu\n", sizeof(a[3]));//16//--sizeof中的表达式在编译时就已经确定结果了,所有后面不会计算了//sizeof在计算变量,数组大小时,是通过类型来推导的,不会真实去访问内存空间的//所以a[3]没有也不会发生越界访问,它的类型就是int(*)[4],那最后就是16
}
总结(只是部分注意点的整理,友友们如果有补充,可以在评论区留言)
- 判断存放的数据类型,像是什么数组或者是字符指针变量存储字符串这种,它们各自有不同的特点;
- 再者就是注意数组名是否是那两种特殊情况(sizeof(数组名)和&数组名),不是的话就是数组首元素地址;
- 注意如果sizeof里面是地址就是4/8(32位环境和64位环境),strlen要注意是否越界和参数类型是否匹配;
- 最好可以画图理解,举个例子,像示这样的图:
数组名的意义:
1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。
三、指针运算笔试题解析
这里关于指针运算,博主放了七道题目,大家自己尝试算一算,解析会配在每道题后面:
题目(1):
#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;
}//程序的结果是什么?
解析:
1、这里&a是数组地址,+1跳过一整个数组到图示位置,将这样的地址存放在ptr中,类型不符合,所以将(&a+1)强制类型转换为(int*)类型;
2、a是数组名,在这里是数组首元素地址,a+1是数组第二个元素的地址,则*(a+1)是第二个元素,也就是2;
3、ptr刚开始在图示&a+1处,-1到达图中所示位置,则*(ptr-1)是此地址处的元素,也就是5;
4、因此程序的结果是: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("%p\n", p + 0x1);printf("%p\n", (unsigned long)p + 0x1);printf("%p\n", (unsigned int*)p + 0x1);return 0;
}
解析:(这里是在X86环境下)
1、先将地址0x100000强制类型转换为(struct Test*)类型存放在p中;
2、第一个printf里面p里存放的地址是(struct Test*)类型,假设结构体的大小是20个字节,+1就跳过20个字节,20转换成16进制就是0x000014,加起来后是0x100014;
3、第二个printf里面将p强制类型转换为(unsigned long)类型,那+1就是直接+1,1转换为16进制就是0x000001,加起来后就是0x100001;
4、第三个printf里面将p强制类型转换为(unsigned int*)类型,+1就是跳过4个字节,4转换为16进制就是0x000004,加起来后就是0x100004;
5、因此最后程序的输出结果是:00100014 00100001 00100004 (每个结果间是会换行的)。
题目(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、 这里要注意数组里的几个元素用的是()而不是{},所以其实是逗号表达式,则int a[3][2] = {1, 3,5};
2、p=a[0]则是数组首元素地址,在二维数组中就是第一行一维数组的数组名;
3、p[0]等价于a[0][0]也就是第一行一维数组的首元素;
4、因此程序的输出结果是 1。
题目(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;
}
解析(这里注意是在x86环境下):
1、根据代码,可以得出p中一行4个元素,再如图中划分出来p和a
2、根据图示找到&p[4][2]和&a[4][2]的位置
3.、地址减地址(指针减指针)的绝对值是它们之间的元素个数,但这里没有绝对值,且明显&a[4][2]靠后,所以&p[4][2] - &a[4][2]小于0,故是-4
4、 根剧printf中的占位符要求,%p需要按16进制打印地址(它认为内存中的补码就是地址),所以先将-4转换成二进制补码形式,再转换为16进制也就是FFFFFFFC,而%d则是按10进制打印,直接就是-4.
5.、所以程序输出的结果是:FFFFFFFC -4图像:
题目(5):
#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;
}
解析:
1、这里分别将&aa + 1和*(aa + 1)强制类型转换为(int*)类型存放在ptr1和ptr2中;
2、&aa是整个二维数组地址,aa是数组名,在这里是数组首元素地址,对于二维数组,首元素地址也就是第一行一维数组地址;
3、+1了,跳过的距离不一样,移动到图中所示位置,且*(aa+1)=aa[1],也就是第二行一维数组首元素地址;
4、*(ptr1 - 1), *(ptr2 - 1),如图所示位置,解引用分别求出来这个地址处的元素;
5、因此程序的输出结果是:10 5
题目(6):
#include <stdio.h>int main()
{char *a[] = {"work","at","alibaba"};char**pa = a;pa++;printf("%s\n", *pa);return 0;
}
解析:
1、根据代码可知,pa存放的是数组a首元素的地址,pa++则得到了数组a第二个元素的地址,即at所在的地址,如图中所示
2、*pa得到了字符串at,直接将它打印出来
3、所以程序的输出结果是:at
图像:
这几道题目做下来,给我们一个很明显的感觉,如果不画图,会很棘手,所以一定要去画图。
最后再来一道题——
题目(7):
#include <stdio.h>int main()
{char *c[] = {"ENTER","NEW","POINT","FIRST"};char**cp[] = {c+3,c+2,c+1,c};char***cpp = cp;printf("%s\n", **++cpp);printf("%s\n", *--*++cpp+3);printf("%s\n", *cpp[-2]+3);printf("%s\n", cpp[-1][-1]+1);return 0;
}
解析:
1、根据题目初始化条件画出下图,明确指向关系;
2、第一个printf:先++cpp,让cpp来到了图示cpp+1的位置,也就是拿到了c+2的地址,再解引用(*++cpp),得到了c+2所指向的POINT的地址,再解引用,拿到了POINT,将它打印出来;
3、第二个printf中:因为第一次printf中已经++cpp了一次,所以现在刚开始在图示cpp+1的位置,再次++cpp,来到了cpp+2的位置,也就是拿到了c+1的地址,解引用拿到c+1所指向的NEW的地址 ,然后 --,拿到了ENTER的地址,再解引用一次,拿到ENTER,最后+3,来到ENTER中的第二个E,所以打印出ER ;
4、第三个printf中:因为前两次的++cpp,现在它在图示cpp+2的位置上,这时的cpp[-2]等价于*(cpp-2),指向的是图示cpp的位置 ,其意义是拿到了c+3的地址后解引用得到了FIRST的地址,再继续解引用一次拿到了FIRST,最后+3来到FIRST中的S,所以打印出ST ;
5、第四个printf中:将cpp[-1][-1]等价于 *(*(cpp-1)-1) ,还是因为两次++cpp,刚开始在图示cpp+2的位置上,先解析*(*(cpp-1)-1),cpp-1拿到了c+2的地址,解引用得到c+2所指向的POINT的地址 再-1得到NEW的地址,解引用后拿到NEW,解析完*(*(cpp-1)-1)也就是cpp[-1][-1],再+1,来到NEW中的E,所以打印出EW ;
6、因此程序的输出结果是:POINT ER ST EW (每个结果间是会换行的) 。图像:
![]()
鸣谢:这里感谢草莓熊大佬的帮助,草莓熊的博客质量很高,感兴趣的友友们可以关注一下草莓熊,这是草莓熊大佬的博客主页:草莓熊Lotso
结论
往期回顾:
C语言指针深入详解(一):内存和地址、指针变量和地址、指针变量类型的意义、指针运算
C语言指针深入详解(二):const修饰指针、野指针、assert断言、指针的使用和传址调用
C语言指针深入详解(三):数组名理解、指针访问数组、一维数组传参的本质、冒泡排序、二级指针、指针数组、指针数组模拟二维数组
C语言指针深入详解(四):指针变量、二维数组传参的本质、函数指针数组、转移表
C语言指针深入详解(五):回调函数、qsort函数
结语:本篇文章就到此结束了,指针部分到这里算是正式收官啦,之后博主有时间会会专门写一篇博客整理指针的知识点大纲、知识点回顾等,这里还是老样子,挖个坑。本文为友友们分享了一些指针题解相关的重要知识点,如果友友们有补充的话欢迎在评论区留言,下一期我们将开启一个新章,介绍字符函数和字符串函数的一些重要知识点,这个部分包括字符分类函数、字符转换函数、strlen的使用和模拟实现、strcpy的使用和模拟实现、strcat的使用和模拟实现、strcmp的使用和模拟实现等等内容,可能得用两篇才能讲完,敬请期待,感谢友友们的关注与支持!