【C语言学习】深入理解指针(5)

1. sizeof和strlen的对比

之前我们简单介绍了sizeof和strlen的一些区别,今天我们更深入讲解他们的不同。

1.1 sizeof

我们先来回顾一下sizeof的知识, sizeof 计算的是变量所占内存空间的大小,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。
sizeof不关心内存中存放的数据是什么,例如:

//sizeof计算变量
//size_t其实专门是设计给sizeof的,表示sizeof的返回值类型

int main()
{
	int a = 0;
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(int));
	printf("%u\n", sizeof(a));
	return 0;
}

运行结果:

那strlen又是怎样的情况呢?

1.2 strlen

strlen是一个标准的C语言库函数,其在cplusplus中的原型如下:

strlen函数统计的是从参数 str 这个地址开始向后,\0 之前字符串中字符的个数。所以strlen 函数会⼀直向后找 \0 字符,直到找到为止,所以可能存在越界访问的情况。
与sizeof相比,strlen是关心内存中存放的数据是什么,没有\0,它会疯的!

int main()
{
	//char arr[] = "abcdef";
	char arr[] = { 'a', 'b', 'c' };
	printf("%d\n", strlen(arr));
	return 0;
}

运行结果:

调试代码:

通过调试出内存窗口,我们可以发现,要找到 ‘\0’,strlen要在字符 ‘c’ 还需要找12个字符!
总结:sizeof和strlen的细致对比
sizeof
1.sizeof是操作符;
2.sizeof计算操作数所占内存的大小,单位是字节;
3. sizeof不关注内存中存放的是什么数据。
strlen
1.strlen是库函数,使用时需要包含头文件 <string.h>;
2.srtlen是求字符串长度,统计的是 ‘\0’ 之前字符的个数;
3.strlen关注的是内存中是否有 ‘\0’,如果没有 ‘\0’,就会持续往后找,可能会导致越界。

讲解完以上内容,我们再来看一段有趣的代码:

int main()
{
	short s = 10;//占2个字节
	int i = 2;//占4个字节
	int n = sizeof(s = i + 4);
	printf("%d\n", n);
	printf("%d\n", s);

运行结果:

为什么会是上面这种结果呢?
别着急,我们慢慢来解释:
1.首先,sizeof 后面的表达式会发生截断,放到s中去,结果仍由s说了算 → 2个字节。
2.其次,sizeof 的操作数是一个表达式,表达式是不参与计算的,所以s仍为10。
为什么表达式不参与计算?
代码编译生成可执行程序,先编译再计算,编译过程执行sizeof,sizeof(s = i + 4) = sizeof(short),相当于跳过了s = i + 4。

通过上面这个例子我们可以得出结论:
表达式存在两个属性:1. 值属性; 2. 类型属性

2. 部分数组和指针笔试题解析

2.1 数组运算题解析

2.1.1 一维数组

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int a[] = { 1, 2, 3, 4 };
	printf("%d\n", sizeof(a));//数组a的大小

	printf("%d\n", sizeof(a + 0));//数组名a并没有单独放在sizeof()内部,也没有&;
	//所以a就是数组首元素的地址,是地址就是4/8个字节
	
	printf("%d\n", sizeof(*a));//a就是首元素的地址, a = &a[0]
	//*a就是第一个元素,也就是a[0],大小就是4个字节

	printf("%d\n", sizeof(a + 1));//4或8—a就是数组首元素的地址(&a[0]—int*),a+1-->&a[1]
	//a+1就是第二个元素的地址

	printf("%d\n", sizeof(a[1]));//4—计算第二个元素的大小,单位是字节

	printf("%d\n", sizeof(&a));//&a—取出的是数组的地址,但是数据的地址也是地址,地址的大小就是4/8个字节
	
	printf("%d\n", sizeof(*&a));//第一种解读:相当于printf("%d\n", sizeof(a))—16
	//第二种解读:
	//&a—int (*p)[4]
	//*p 访问一个数组的大小
	//p+1 跳过一个数组的大小

	printf("%d\n", sizeof(&a + 1));//跳过一个数组后的地址,是地址就是4/8个字节

	printf("%d\n", sizeof(&a[0]));//首元素的地址,4/8

	printf("%d\n", sizeof(&a[0] + 1));//第二个元素的地址
	//&a[1]
	//&a[0] - int*
	return 0;
}

运行结果:

2.1.2 字符数组

(一)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
	char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
	printf("%d\n", sizeof(arr));//6

	printf("%d\n", sizeof(arr + 0));//arr是数组首元素的地址,arr+0还是首元素的地址,地址大小就是4/8个字节
	
	printf("%d\n", sizeof(*arr));//arr还是首元素的地址,*arr就是首元素,就占1个字节
	
	printf("%d\n", sizeof(arr[1]));//arr[1]就是数组的第二个元素,大小是1个字节
	
	printf("%d\n", sizeof(&arr));//&arr是数组的地址,数组的地址也是地址,大小就是4/8个字节
	
	printf("%d\n", sizeof(&arr + 1));//跳过一个数组后的地址,是地址就是4/8个字节
	
	printf("%d\n", sizeof(&arr[0] + 1));//第二个元素的地址

	return 0;
}

运行结果:

(二)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
	char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
	printf("%d\n", strlen(arr));//随机值,因为数组中没有明确给出\0

	printf("%d\n", strlen(arr + 0));//随机值
	
	printf("%d\n", strlen(*arr));//*arr ——> 'a' ——> 97,非法访问
	
	printf("%d\n", strlen(arr[1]));//非法访问
	
	printf("%d\n", strlen(&arr));//随机值
	//&arr                const char*
	//char (*p)[6]        const char*
	
	printf("%d\n", strlen(&arr + 1));//随机值(与前面的随机值差6)
	
	printf("%d\n", strlen(&arr[0] + 1));//随机值

	return 0;
}

(三)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main()
{
	char arr[] = "abcdef";
	//[a b c d e f \0]
	
	printf("%d\n", sizeof(arr));//7
	
	printf("%d\n", sizeof(arr+0));//arr是数组首元素的地址,arr+0还是首元素的地址,地址大小就是4/8个字节
	
	printf("%d\n", sizeof(*arr));//arr表示首元素的地址,*arr就是首元素,大小是1个字节
	
	printf("%d\n", sizeof(arr[1]));//arr[1]就是数组第二个元素,大小也是1个字节
	
	printf("%d\n", sizeof(&arr));//&arr是数组的地址,大小是4/8个字节
	
	printf("%d\n", sizeof(&arr+1));//跳过1个数组后的地址,是地址就是4/8个字节
	
	printf("%d\n", sizeof(&arr[0]+1));//第二个元素的地址,大小是4/8个字节
	
	return 0;
}

运行结果:

(四)


int main()
{
	char arr[] = "abcdef";
	//[a b c d e f \0]
	
	printf("%d\n", strlen(arr));//6 arr是首元素的地址
	
	printf("%d\n", strlen(arr+0));//6 arr + 0也是首元素的地址
	
	printf("%d\n", strlen(*arr));//error - 非法访问
	
	printf("%d\n", strlen(arr[1]));//error - 非法访问
	
	printf("%d\n", strlen(&arr));//6 &arr是数组的地址,但是这个地址也是指向数组的起始位置
	
	printf("%d\n", strlen(&arr+1));//跳过整个数组后的地址,从这里开始找\0,就是随机值
	
	printf("%d\n", strlen(&arr[0]+1));//第二个元素的地址,长度是5
	
	return 0;
}

运行结果:

(五)

int main()
{
	char* p = "abcdef";
	//[a b c d e f \0]

	printf("%d\n", sizeof(p));//p是一个指针变量,大小是4/8个字节
	
	printf("%d\n", sizeof(p+1));//第二个元素b的地址,大小是4/8个字节
	
	printf("%d\n", sizeof(*p));//*p指向第一个元素a,大小是1个字节
	
	printf("%d\n", sizeof(p[0]));//p[0] ——> *(p + 0),其实就是字符串中的首字符,大小是1个字节
	//字符串可以看成是一个数组,p是数组首元素a的地址,p[0]其实就是访问数组中的元素a,大小是1个字节
	
	printf("%d\n", sizeof(&p));//&p是p的地址,是地址大小就是4/8个字节
	
	printf("%d\n", sizeof(&p+1));//&p + 1也是地址,&p + 1跳过p变量后的地址
	
	printf("%d\n", sizeof(&p[0]+1));//4/8 - 第二个元素b的地址
	
	return 0;
}

运行结果:

(六)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	char *p = "abcdef";

	printf("%d\n", strlen(p));//6 - 字符串中有\0,从p中存放的是a的地址,从a的地址开始向后访问

	printf("%d\n", strlen(p + 1));//5 - 从b的地址开始向后访问

	printf("%d\n", strlen(*p));//error - 非法访问

	printf("%d\n", strlen(p[0]));//error - 非法访问

	printf("%d\n", strlen(&p));//随机值,&p是p的地址,从p所占空间的起始位置查找

	printf("%d\n", strlen(&p + 1));//随机值

	printf("%d\n", strlen(&p[0] + 1));//5

	return 0;
}

运行结果:

2.1.2 二维数组

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <assert.h>

int main()
{
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//计算的是整个二维数组的大小,单位是字节—48

	printf("%d\n", sizeof(a[0][0]));//a[0][0]-第一个元素的大小,大小是4个字节

	printf("%d\n", sizeof(a[0]));//16 - a[0]其实是第一行的数组名,这里第一行的数组名单独放在sizeof内部了,计算的是第一行的大小

	printf("%d\n", sizeof(a[0] + 1));//
	//a[0]是第一行这个数组的数组名,但是数组名并没有单独放在sizeof内部,所以数组名表示数组首元素的地址,也就是a[0][0]的地址,a[0] + 1是第一行第二个元素(a[0][1])的地址

	printf("%d\n", sizeof(*(a[0] + 1)));//a[0] + 1是第一行第二个元素(a[0][1])的地址
	//*(a[0] + 1)是第一行第二个元素,大小是4个字节

	printf("%d\n", sizeof(a + 1));//a是二维数组的数组名,没有单独放在sizeof的内部,所以a就是数组首元素的地址,也就是第一行的地址
	//a + 1就是第二行的地址
	//a -- int (*)[4]
	//a + 1 -- int (*)[4]

	printf("%d\n", sizeof(*(a + 1)));//16 --> *(a + 1) == a[1]


	printf("%d\n", sizeof(&a[0] + 1));//第二行的地址,是地址大小就是4/8个字节
	//a[0]是第一行的数组名,&a[0]取出的是第一行的地址,&a[0] + 1得到的就是第二行的地址

	printf("%d\n", sizeof(*(&a[0] + 1)));//16

	printf("%d\n", sizeof(*a));//16—数组名a就是数组首元素的地址,也就是第一行的地址,*a就是第一行的大小
	//*a == *(a + 0) == a[0]

	printf("%d\n", sizeof(a[3]));//16
	//arr[3] == arr[0]
	return 0;
}

运行结果:

通过上述实例,我们可以先总结一波:
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3.除了以上两种情况以外,其余所有的数组名都表示首元素的地址。

2.2 指针运算笔试题解析

(一)

#define _CRT_SECURE_NO_WARNINGS
#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;
}

运行结果:

代码分析:

(二)
在X86环境下,假设结构体的大小是20个字节,下列程序输出的结果是啥?

#define _CRT_SECURE_NO_WARNINGS
#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);//0x1 == 1
	//0x100014        不是0x100020,需要转换成16进制数字

	printf("%p\n", (unsigned long)p + 0x1);//p被强制类型转换成整型—0x100001

	printf("%p\n", (unsigned int*)p + 0x1);//0x100004

	return 0;
}

运行结果:

(三)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
	int a[3][2] = { (0, 1), (2, 3), (4, 5) };//逗号表达式从左向右依次计算,但是整个表达式的结果是最后一个表达式的结果。
	//1 3
	//5 0
	//0 0
	int *p;
	p = a[0];
	printf("%d\n", p[0]);
	return 0;
}

运行结果:

代码解析:

(四)重点!!!

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.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;
}

运行结果:

代码解析:

p[4][2]可以看成是(p + 4)[2],两个指针相减得到的是两个之间的元素个数,即4。但是由于&p[4][2]的地址小于&a[4][2],所以结果为-4。
而-4在内存中是以补码的形式存放的:
1000 0000 0000 0000 0000 0000 0000 0100 — 原码
1111 1111 1111 1111 1111 1111 1111 1011 — 反码
1111 1111 1111 1111 1111 1111 1111 1100 — 补码
F F F F F F F C — 地址
(五)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.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;
}

运行结果:

代码解析:

(六)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

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

运行结果:

代码解析:

(七)

#define _CRT_SECURE_NO_WARNINGS
#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指向 c + 3,++cpp后指向 c + 2,解引用两次后指向"POINT"。

*- -*++cpp + 3

cpp经过上述操作指向 c + 2,经过++cpp后,指向 c + 1,解引用再经过- -cpp,指向c,解引用两次后指向"ENTER",加3后指向"ER"。

*cpp[-2] + 3

cpp[-2]等价于 *(cpp - 2),刚才我们的cpp指向 c + 1,减2后指向 c + 3 ,解引用后指向c[3],再次解引用后指向"FIRST",加3后指向"ST"。

cpp[-1][-1] + 1

cpp[-1][-1]我们可以看作是*(*(cpp - 1) - 1),刚才cpp指向 c + 3,减1后指向 c + 2,解引用后指向c[2],减1之后指向c[1],c[1]解引用后指向"NEW",加1后指向"EW"。

好了,以上就是本期博客的全部内容了,这也是指针部分最后一期内容,看在我肝这么久的份上,还请小伙伴们多多支持。另外,由于工作原因,没办法做到像以前那样频繁更新,还请小伙伴们理解🤝,让我们一起在代码的世界里遨游。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值