C语言之指针进阶(5),sizeof和strlen的数组计算以及指针运算笔试难题详解


目录

前言

一、sizeof和strlen 的区分比较

二、sizeof,strlen与数组的计算

三、指针运算,笔试难题解析

总结


前言

        本文作为指针进阶的最后一篇文章,给大家带来了丰富的例题,这其中包括区分比较sizeof和strlen计算各种花样的数组指针表达式,如果你能答对所有的关于sizeof和strlen的计算例题,那么关于sizeof和strlen的计算你就无敌了。另外最主要的还是指针的运算笔试难题,这些笔试真题就能帮我们更深入的理解指针,最终成为C语言大佬,当然指针还未结束,最后还是需要自己理解和积累,希望本文对大家有所帮助


一、sizeof和strlen 的区分比较

sizeofstrlen
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";//字符串赋值末尾自带一个\0
	char ch2[] = { 'a','b','c','d','e','f' };//末尾没有\0
	char ch3[] = { 'a','b','c','d','e','f' ,'\0' };//末尾手动添加\0

	printf("%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,解引用首元素地址,指向字符a
	printf("%zd\n", sizeof(arr[1]));//1,指向字符b
	printf("%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));//随机值,等于第一行减6
	printf("%zd\n", strlen(&arr[0] + 1));//随机值,等于第一行减1

	return 0;
}

例4:

#include <stdio.h>

int main()
{
	char arr[] = "abcdef";//以字符串字面量进行赋值,末尾有隐藏了的\0

	printf("%zd\n", sizeof(arr));//7,计算包括了\0在内的7个字符
	printf("%zd\n", sizeof(arr + 0));//4/8,非数组名单独放在sizeof里,等价于&arr[0]
	printf("%zd\n", sizeof(*arr));//1,解引用首元素的地址,指向字符a
	printf("%zd\n", sizeof(arr[1]));//1,指向b
	printf("%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";//末尾有\0

	printf("%zd\n", strlen(arr));//6
	printf("%zd\n", strlen(arr + 0));//6
	printf("%zd\n", strlen(*arr));//访问地址为97处的内存空间,程序崩溃
	printf("%zd\n", strlen(arr[1]));//访问地址为98处的内存空间,程序崩溃
	printf("%zd\n", strlen(&arr));//6
	printf("%zd\n", strlen(&arr + 1));//跳过该数组,越界访问,返回随机值
	printf("%zd\n", strlen(&arr[0] + 1));//5

	return 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,指向字符a
	printf("%zd\n", sizeof(p[0]));//1,字符a
	printf("%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));//6
	printf("%zd\n", strlen(p + 1));//5
	printf("%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));//5

	return 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;
}

运行结果:

画图解析:

  1. 因为&a得到一个数组指针,+1就跳过一个数组,指向了a[5]的末尾,因此(&a+1)应指向如图所示的位置,因为此时(&a+1)还是一个数组指针,因此强制转换为 int* ,再传给ptr
  2. 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;
}

运行结果:

解析:

  1. 首先定义了一个结构体指针p,p储存的地址就是被强转为结构体指针类型的0x100000
  2. 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
  3. printf("%p\n", (unsigned long)p + 0x1), (unsigned long)p将结构体指针类型的p强制转换为无符号整形p,其中储存的地址就会变为无符号整数,因此最终结果就是两个整数相加,也就是 0x100000+0x1 = 0x100001 ,最后以地址的格式打印出来就是 00100001
  4. 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;
}

运行结果:

解析:

  1. 首先我们需要关注{ (0, 1), (2, 3), (4, 5) },这里面是3个逗号表达式,逗号表达式结果取决于其最后一位,所以大括号里面实际只有 1,3,5,这三个数
  2. 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;
}

运行结果:

解析:

  1. 如图所示,a本为 int (*)[5] 类型,却强制赋值给p int (*)[4] 类型,导致两者出现上图分配情况,通过画图我们不难找到 p[4][2] 和 a[4][2] 的位置
  2. 我们知道数组中两指针相减,那么得到的就是两指针之间的元素个数,因为p[4][2]地址小于a[4][2],所以得到的是 -4,-4以%d的格式打印就是-4,但是-4以%p打印就不一样了
  3. -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;
}

运行结果:

解析:

  1. (int*)(&aa + 1),&aa+1取出整个数组的地址,可以理解为二维数组的数组指针,+1就跳过整个数组,来到数组的末尾,然后强转为(int*)类型,赋给ptr1
  2. (int*)(*(aa + 1)),可以直接理解为(int*)aa[1],*(aa+1)就表示跳过一个元素解引用,也就是aa[1],指向的就是第二个数组元素的首地址,赋给ptr2
  3. 因此,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;
}

运行结果:

解析:

  1. a是一个字符指针数组,它里面3个元素分别对应后面三个字符串字面量的首字符地址
  2. 因为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是三级指针,接收二级指针变量的地址)

  1. 首先第一处打印,**++cpp,*与前置++优先级相同,结合性从右到左,因此cpp先自增1,cpp就指向cp[1]的地址,然后*++cpp,解引用得到的就是cp[1]的内容c+2,最后* *++cpp,再解引用,就是解引用c+2,指向的就是c[2]的内容,也就是POINT\0的首字符地址,最后以%s打印就是POINT。由于cpp指向发生变化,上图的需进行修改
  2. 第二处打印,*-- * ++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。由于以上变化,我们再重新绘图
  3. 第三处打印,*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。由于上述操作并未实质改变指针指向的内容,只是表达式的计算,所以不需重新绘图
  4. 第四处打印,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。


总结

        至此,我就解析完了本文的所有题目,希望对大家有所帮助,也很感谢大家的支持,大家有什么疑问欢迎评论区指出

  • 38
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值