目录
前言
一、sizeof和strlen 的区分比较
二、sizeof,strlen与数组的计算
三、指针运算,笔试难题解析
总结
前言
本文作为指针进阶的最后一篇文章,给大家带来了丰富的例题,这其中包括区分比较sizeof和strlen计算各种花样的数组指针表达式,如果你能答对所有的关于sizeof和strlen的计算例题,那么关于sizeof和strlen的计算你就无敌了。另外最主要的还是指针的运算笔试难题,这些笔试真题就能帮我们更深入的理解指针,最终成为C语言大佬,当然指针还未结束,最后还是需要自己理解和积累,希望本文对大家有所帮助
一、sizeof和strlen 的区分比较
sizeof | strlen |
1.sizeof是操作符 | 1.strlen是库函数,使用时需包含头文件<string.h> |
2.sizeof计算操作数所占的内存大小, 单位是字节,返回类型为size_t | 2.strlen是计算字符串长度的,统计的是\0之前字符 的个数,返回类型为size_t |
3.sizeof不关注内存中存放的数据,sizeof中 如果是表达式也不会真正的被计算 | 3.关注内存中是否有\0,如果没有\0,就会 继续往后找,可能会导致越界访问 |
4.sizeof 传入的参数可以是变量名, 可以是类型名,也可以是整数、浮点数等 | 4.strlen 的形参是一个字符指针,也就是 需要计算的字符串首元素地址 |
strlen的形参
sizeof不关注数据内容体现在以下代码:
#include <stdio.h>int main()
{int a = 10;printf("%zd\n", sizeof(a));//计算a大小printf("%zd\n", sizeof(int));//直接计算类型大小printf("%zd\n", sizeof(10));//甚至直接计算整数大小return 0;
}
运行结果:
strlen关注内存中是否有\0体现在以下代码:
#include <stdio.h>
#include <string.h>int main()
{char ch1[] = "abcdef";//字符串赋值末尾自带一个\0char ch2[] = { 'a','b','c','d','e','f' };//末尾没有\0char ch3[] = { 'a','b','c','d','e','f' ,'\0' };//末尾手动添加\0printf("%zd\n", strlen(ch1));printf("%zd\n", strlen(ch2));printf("%zd\n", strlen(ch3));return 0;
}
运行结果:
出现38的结果就是因为 ch2 数组中没有\0,strlen只能在数组后面的内存中去寻找\0,也就是越界访问了,最终会返回一个随机值
二、sizeof,strlen与数组的计算
以下就是使用 sizeof 和 strlen 计算数组的题目,你能答对几道?可不要小看这些计算,一不小心就会犯错误,重要的还是理解。
注意:以下涉及的知识与我主页指针进阶(1)数组与指针有关,即数组名为数组首元素地址,但有两个例外:
1. sizeof(数组名),数组名单独放在sizeof中,此时数组名表示整个数组,计算的是整个数组大小
2. &数组名,取出的是整个数组的地址,也就是一个数组指针
如不了解,可前去预览,以便更好的理解以下代码
注:以下代码中行末尾注释的数字为每一行的答案,4/8表示在x86或x64位平台下不同的结果
例1:小试牛刀
#include <stdio.h>int main()
{int a[] = { 1,2,3,4 };printf("%zd\n", sizeof(a));//16,a单独放在sizeof中表示计算整个数组大小printf("%zd\n", sizeof(a + 0));//4/8,非单独表示指针printf("%zd\n", sizeof(*a));//4,解引用首元素地址printf("%zd\n", sizeof(a + 1));//4/8 等价于&a[1]printf("%zd\n", sizeof(a[1]));//4 printf("%zd\n", sizeof(&a));//4/8,&a表示取出的是整个数组的地址,是一个数组指针printf("%zd\n", sizeof(*&a));//16,*与&抵消,相当于a单独放在sizeof中printf("%zd\n", sizeof(&a + 1));//4/8,数组指针加1,表示跳过整个数组的后一个数组指针 printf("%zd\n", sizeof(&a[0]));//4/8,取出第一个元素地址printf("%zd\n", sizeof(&a[0] + 1));//4/8,等价于&a[1]return 0;
}
例2:
#include <stdio.h>int main()
{char arr[] = { 'a','b','c','d','e','f' };printf("%zd\n", sizeof(arr));//6,数组名单独放在sizeof里,表示计算整个数组大小printf("%zd\n", sizeof(arr + 0));//4/8,非单独放,表示首元素地址printf("%zd\n", sizeof(*arr));//1,解引用首元素地址,指向字符aprintf("%zd\n", sizeof(arr[1]));//1,指向字符bprintf("%zd\n", sizeof(&arr));//4/8,取出的是一个数组指针printf("%zd\n", sizeof(&arr + 1));//4/8,还是一个数组指针printf("%zd\n", sizeof(&arr[0] + 1));//4/8,相当于&arr[1]return 0;
}
例3:
#include <stdio.h>
#include <string.h>int main()
{char arr[] = { 'a','b','c','d','e','f' };printf("%zd\n", strlen(arr));//随机值printf("%zd\n", strlen(arr + 0));//随机值,等于第一行printf("%zd\n", strlen(*arr));//访问地址为97处的内存,程序崩溃printf("%zd\n", strlen(arr[1]));//访问地址为98处的内存,程序崩溃printf("%zd\n", strlen(&arr));//随机值,等于第一行printf("%zd\n", strlen(&arr + 1));//随机值,等于第一行减6printf("%zd\n", strlen(&arr[0] + 1));//随机值,等于第一行减1return 0;
}
例4:
#include <stdio.h>int main()
{char arr[] = "abcdef";//以字符串字面量进行赋值,末尾有隐藏了的\0printf("%zd\n", sizeof(arr));//7,计算包括了\0在内的7个字符printf("%zd\n", sizeof(arr + 0));//4/8,非数组名单独放在sizeof里,等价于&arr[0]printf("%zd\n", sizeof(*arr));//1,解引用首元素的地址,指向字符aprintf("%zd\n", sizeof(arr[1]));//1,指向bprintf("%zd\n", sizeof(&arr));//4/8,取出的是数组指针printf("%zd\n", sizeof(&arr + 1));//4/8,还是一个数组指针printf("%zd\n", sizeof(&arr[0] + 1));//4/8,等价于&arr[1]return 0;
}
例5:
#include <stdio.h>
#include <string.h>int main()
{char arr[] = "abcdef";//末尾有\0printf("%zd\n", strlen(arr));//6printf("%zd\n", strlen(arr + 0));//6printf("%zd\n", strlen(*arr));//访问地址为97处的内存空间,程序崩溃printf("%zd\n", strlen(arr[1]));//访问地址为98处的内存空间,程序崩溃printf("%zd\n", strlen(&arr));//6printf("%zd\n", strlen(&arr + 1));//跳过该数组,越界访问,返回随机值printf("%zd\n", strlen(&arr[0] + 1));//5return 0;
}
例6:
#include <stdio.h>int main()
{char* p = "abcdef";//p接收的是字符串的首元素地址printf("%zd\n", sizeof(p));//4/8 注意指针变量就是指针,数组名是数组名,这两者这不一样printf("%zd\n", sizeof(p + 1));//4/8,等价于&p[1]printf("%zd\n", sizeof(*p));//1,指向字符aprintf("%zd\n", sizeof(p[0]));//1,字符aprintf("%zd\n", sizeof(&p));//4/8,指针变量的地址,相当于一个二级指针printf("%zd\n", sizeof(&p + 1));//4/8,还是一个二级指针printf("%zd\n", sizeof(&p[0] + 1));//4/8,字符串中b的地址return 0;
}
例7:
#include <stdio.h>
#include <string.h>int main()
{char* p = "abcdef";printf("%zd\n", strlen(p));//6printf("%zd\n", strlen(p + 1));//5printf("%zd\n", strlen(*p));//访问地址为97处的内存空间,程序崩溃printf("%zd\n", strlen(p[0]));//访问地址为97处的内存空间,程序崩溃printf("%zd\n", strlen(&p));//传入的是p变量本身的地址,返回随机值printf("%zd\n", strlen(&p + 1));//传入的是跳过b变量地址的地址,返回随机值printf("%zd\n", strlen(&p[0] + 1));//5return 0;
}
例8:二维数组
#include <stdio.h>int main()
{int a[3][4] = { 0 };printf("%zd\n", sizeof(a));//48,数组名单独放在sizeof中,计算的是整个数组大小printf("%zd\n", sizeof(a[0][0]));//4,表示第一行第一个元素printf("%zd\n", sizeof(a[0]));//16,a[0]表示第一行数组的数组名,单独放在sizeof中printf("%zd\n", sizeof(a[0] + 1));//4/8,等价于&a[0][1]printf("%zd\n", sizeof(*(a[0] + 1)));//4,等价于a[0][1]printf("%zd\n", sizeof(a + 1));//4/8,a表示数组首元素地址也就是&a[0],a+1就是&a[1]printf("%zd\n", sizeof(*(a + 1)));//16,继上一行,a+1再解引用相当于a[1]数组名单独放sizeof中printf("%zd\n", sizeof(&a[0] + 1));//4/8,等价于&a[1]printf("%zd\n", sizeof(*(&a[0] + 1)));//16,继上一行,&与*抵消,相当于a[1]单独放在sizeof中printf("%zd\n", sizeof(*a));//16,等价于*&a[0],相当于a[0]数组名单独放在sizeof中printf("%zd\n", sizeof(a[3]));//16,这里一定记住:sizeof中的表达式不会真正计算,这里不会越界 访问,因此依旧表示数组名a[3]单独放在sizeof中,计算的是a[3]整个数组大小return 0;
}
注意:sizeof()中的表达式不会真正的被计算
例如:
三、指针运算,笔试难题解析
题目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;
}
运行结果:
画图解析:
- 因为&a得到一个数组指针,+1就跳过一个数组,指向了a[5]的末尾,因此(&a+1)应指向如图所示的位置,因为此时(&a+1)还是一个数组指针,因此强制转换为 int* ,再传给ptr
- int* 类型指针-1往低地址处移动4个字节,所以ptr-1指向的就是5,而 *(a+1) 就等价于 a[1],指向的是数组第二个元素2
题目2:
//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?
#include <stdio.h>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;
}
运行结果:
解析:
- 首先定义了一个结构体指针p,p储存的地址就是被强转为结构体指针类型的0x100000
- printf("%p\n", p + 0x1),0x1表示16进制数字1,所以p+0x1就是p+1,p为一个结构体类型的指针变量,+1就是跳过一个结构体大小的字节,而结构体大小就是20个字节,20转换为16进制就是0x14,所以p+1 = 0x100000+0x14 = 0x100014,%p打印,会打印完整地址,因此不足位前面补0,最终结果就是 00100014
- printf("%p\n", (unsigned long)p + 0x1), (unsigned long)p将结构体指针类型的p强制转换为无符号整形p,其中储存的地址就会变为无符号整数,因此最终结果就是两个整数相加,也就是 0x100000+0x1 = 0x100001 ,最后以地址的格式打印出来就是 00100001
- printf("%p\n", (unsigned int*)p + 0x1),这里就是将p强转为无符号整形指针类型,+1跳过一个无符号整形大小的字节,也就是4个字节,因此最终结果就是 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;
}
运行结果:
解析:
- 首先我们需要关注{ (0, 1), (2, 3), (4, 5) },这里面是3个逗号表达式,逗号表达式结果取决于其最后一位,所以大括号里面实际只有 1,3,5,这三个数
- a[0]就为第一行元素的首地址,所以p[0] 等价于 *(p+0),也就是指向第一行第一个元素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;
}
运行结果:
解析:
- 如图所示,a本为 int (*)[5] 类型,却强制赋值给p int (*)[4] 类型,导致两者出现上图分配情况,通过画图我们不难找到 p[4][2] 和 a[4][2] 的位置
- 我们知道数组中两指针相减,那么得到的就是两指针之间的元素个数,因为p[4][2]地址小于a[4][2],所以得到的是 -4,-4以%d的格式打印就是-4,但是-4以%p打印就不一样了
- -4以%p打印,打印的是-4在内存中的补码,以地址的格式打印。-4的补码就为1111 1111 1111 1111 1111 1111 1111 1100,每四个二进制位以地址的16进制打印就是 FFFFFFFC
题目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;
}
运行结果:
解析:
- (int*)(&aa + 1),&aa+1取出整个数组的地址,可以理解为二维数组的数组指针,+1就跳过整个数组,来到数组的末尾,然后强转为(int*)类型,赋给ptr1
- (int*)(*(aa + 1)),可以直接理解为(int*)aa[1],*(aa+1)就表示跳过一个元素解引用,也就是aa[1],指向的就是第二个数组元素的首地址,赋给ptr2
- 因此,ptr1,ptr2都被强转为int*类型,这样-1就往地址跳过一个整形大小的字节,也就是分别指向10和5
题目6:
//程序运行的结果是啥?
#include <stdio.h>int main()
{char* a[] = { "work","at","alibaba" };char** pa = a;pa++;printf("%s\n", *pa);return 0;
}
运行结果:
解析:
- a是一个字符指针数组,它里面3个元素分别对应后面三个字符串字面量的首字符地址
- 因为a是数组首元素地址,它指向的是一个字符指针,因此接收a需要一个二级指针变量,也就是pa,给pa赋值a,pa开始指向的是a[0]的地址,pa++后,pa向后移动一个地址,指向了a[1]的地址,因此*pa就等于a[1],以%s打印字符串需要字符串首元素地址,a[1]储存的是at\0的首元素地址,因此最终打印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;
}
运行结果:
解析:(cpp是三级指针,接收二级指针变量的地址)
- 首先第一处打印,**++cpp,*与前置++优先级相同,结合性从右到左,因此cpp先自增1,cpp就指向cp[1]的地址,然后*++cpp,解引用得到的就是cp[1]的内容c+2,最后* *++cpp,再解引用,就是解引用c+2,指向的就是c[2]的内容,也就是POINT\0的首字符地址,最后以%s打印就是POINT。由于cpp指向发生变化,上图的需进行修改
- 第二处打印,*-- * ++cpp + 3,我们来一步一步分析,以优先级和结合性,首先是++cpp,那么cpp自增1就指向了cp[2]的地址了,然后*++cpp,解引用cp[2]的地址,得到的就是cp[2]指向的内容c+1,然后--*++cpp,c+1自减1,就是把cp[2]的内容从c+1修改为c,然后*--*++cpp,再解引用,这时解引用的是c,指向的就是c[0]的内容,也就是ENTER\0的首字符地址,最后*--*++cpp+3,加3表示跳过3个字节(因为指针为char类型),此时指向的就是字符E的地址,因此最终打印的结果就是ER。由于以上变化,我们再重新绘图
- 第三处打印,*cpp[-2] + 3 ==>(等价于) **(cpp-2)+3,首先cpp-2,改变指向的内容为cp[0]的地址,然后*(cpp-2),解引用得到cp[0]的内容c+3,然后**(cpp-2),再解引用得到的就是c+3也就是c[3]所指向的内容,也就是FIRST\0的首字符地址,最后**(cpp-2)+3,加3跳过3个字节,指向字符S的地址,最终打印的就是ST。由于上述操作并未实质改变指针指向的内容,只是表达式的计算,所以不需重新绘图
- 第四处打印,cpp[-1][-1] + 1 ==> *(*(cpp-1))-1)+1,首先cpp-1,指向的是cp[1]的地址,然后*(cpp-1),解引用得到cp[1]指向的内容c+2,然后*(cpp-1)-1,减一表示把c+2减1,导致改变cp[1]中的内容从c+2变为c+1,然后*(*(cpp-1)-1),再解引用c+1,也就是解引用c[1]的内容,c[1]的内容指向的是字符串字面量NEW\0的首字符地址,最后*(*(cpp-1))-1)+1,加1表示跳过一个字节,此时指向的就是字符E的地址,最终打印的结果就是EW。
总结
至此,我就解析完了本文的所有题目,希望对大家有所帮助,也很感谢大家的支持,大家有什么疑问欢迎评论区指出