C语言——小知识和小细节14

一、一维数组与 sizeof

对于 sizeof 的参数,输入不同的与数组相关的参数,sizeof 返回的结果是不同的,下面给出一些例子与分析:

cpp 复制代码
#include <stdio.h>

int main()
{
	int a[] = { 1,2,3,4 };

	printf("%zu\n", sizeof(a));
	//sizeof(数组名),就是数组名的特例,会返回整个数组的大小,这里数组大小为16字节

	printf("%zu\n", sizeof(a + 0));
	//这里a + 0就相当于&a[0],这里就是一个地址,所以会返回指针的大小,这里用的是64为平台,指针大小为8字节

	printf("%zu\n", sizeof(*a));
	//这里a代表数组首元素地址,所以在解引用后就是首元素,首元素是整型,所以大小是4字节

	printf("%zu\n", sizeof(a + 1));
	//与上面的a + 0相似,只不过这里是第二个元素的地址,所以还是8字节

	printf("%zu\n", sizeof(a[0]));
	//a[0]就是数组首元素,所以是4字节

	printf("%zu\n", sizeof(&a));
	//这里的&a是数组指针,也是指针,所以是8字节

	printf("%zu\n", sizeof(*&a));
	//&a是整个数组的地址,然后解引用,访问的就是整个数组,所以是整个数组的大小,所以是16字节

	printf("%zu\n", sizeof(&a + 1));
	//&a是整个数组的地址,然后加一,就是跳过一个a数组大小的步长后的地址,但本质还是一个指针,所以是8字节

	printf("%zu\n", sizeof(&a[0]));
	//&a[0]就是首元素地址,所以还是地址,所以是8字节

	printf("%zu\n", sizeof(&a[0] + 1));
	//&a[0]是首元素指针,加一后就是第二个元素的指针,还是指针,所以是8字节


	return 0;
}

运行结果:

可以看到与数组相关的参数经过 sizeof 运算符后,得到的结果是不尽相同的。

二、字符数组与 strlen

我们知道 strlen 函数可以求字符串长度,对于不同的字符数组相关的参数传入strlen 函数,strlen 函数返回的值是不尽相同的:

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };

	printf("%zu\n", strlen(arr));
	//这里的arr是数组首元素地址,因为 strlen 函数只有遇到 \0 才会停止,这里的数组中没有 \0 ,所以只有遇到野生的 \0 时才会停止,这里的值是大于等于6的随机整数

	printf("%zu\n", strlen(arr + 0));
	//arr + 0就相当于&arr[0],所以是首元素地址,这里也是一样,只有遇到野生的 \0 时才会停止,所以也是一个大于等于6的随机整数

	printf("%zu\n", strlen(&arr[0] + 1));
	//这里的&arr[0] + 1是数组的第二个元素的地址,只有遇到野生的 \0 时才会停止,所以这里是大于5的随机整数
	
	return 0;
}

运行结果:

可以看到,strlen 函数返回的值是不尽相同的。

一些错误示范:

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
	char arr[] = { 'a','b','c','d','e','f' };

	printf("%zu\n", strlen(*arr));
	//这里*arr是数组首元素,而数组首元素是一个字符,对于 strlen 函数的参数是 const char* _Ptr 所以这里的参数是错的
	
	printf("%zu\n", strlen(arr[1]));
	//与上面一样这里的arr[1]是数组元素,一个字符,参数是错误的
	
	printf("%zu\n", strlen(&arr));
	//这里的&arr是整个数组的地址,类型是 char(*)[6],与 const char* 这个类型实不符的,所以这里的参数也是错误的
	
	printf("%zu\n", strlen(&arr + 1));
	//这里的&arr是整个数组的地址,加一就是跳了一个数组的步长,类型依旧是 char(*)[6],与 const char* 这个类型实不符的,所以这里的参数依旧是错误的

	return 0;
}

这些都是错误的,因为传给 strlen 函数的参数的类型与 strlen 函数需要的参数类型不匹配。

因为我们知道,strlen 的参数是 const char* 类型的,而这里错误示范的前两个传的参数是字符型数据,我们可以进行详细分析:

cpp 复制代码
printf("%zu\n", strlen(*arr));

这里的 arr 代表首元素地址,解引用后就是首元素,而首元素是字符 a ,所以传给 strlen 的参数是字符 a ,而字符 a 的 ASIIC 码值是97,所以这里 strlen 就将对97这个地址进行访问,并试图在这个地址上查找一个字符串,这导致了未定义行为。对于97这个地址,这个地址在大多数操作系统上是不合法的或未分配给你的程序的,因此尝试访问它导致了访问违规错误或段错误。

后面的两个错误示范都是传了整个数组的地址,类型是 char(*)[6],与 const char* 这个类型实不符的,所以这里的参数也是错误的。

三、二维数组与 sizeof

对于 sizeof 的参数,输入不同的与二维数组相关的参数,sizeof 返回的结果是不同的,下面给出一些例子与分析:

cpp 复制代码
#include <stdio.h>

int main()
{
	int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };

	printf("%zu\n", sizeof(arr));
	//这里是数组名的特例,在sizeof中只放一个数组名,返回的值是整个数组的大小,所以这里是48字节
	
	printf("%zu\n", sizeof(arr[0][0]));
	//这里arr[0][0]是二位数组第一个子数组的第一个元素,是一个整型,所以是4字节

	printf("%zu\n", sizeof(arr[0]));
	//这里arr[0]代表二位数组首元素的数组名,而二维数组首元素也是数组,是一维数组,是其第一个子数组,而这里arr[0]是代表子数组的数组名,数组名单个放到sizeof中会返回整个数组的大小,而对于这里的二位数组,其子数组的有四个元素,就是有四个整型,所以返回子数组的大小是16字节

	printf("%zu\n", sizeof(arr[0] + 1));
	//这里的arr[0]不是单独放到sizeof中的,所以是指向第一个子数组的第一个元素的指针,相当于&arr[0][0],所以arr[0] + 1则是指向第一个子数组的第二个元素的指针,相当于&arr[0][0] + 1,既然是指针,这里就是8字节

	printf("%zu\n", sizeof(*(arr[0] + 1)));
	//根据上面的解释,这里的*(arr[0] + 1)就代表第一个子数组第二个元素,所以是4字节

	printf("%zu\n", sizeof(arr + 1));
	//这里的arr数组名没有单独放到sizeof中,所以指的是其第一个子数组的地址,在加一后,就是第二个子数组的地址,所以是8字节

	printf("%zu\n", sizeof(*(arr + 1)));
	//因为arr + 1是二维数组的第二个子数组的地址,所以解引用后访问的就是整个第二个子数组,所以是16字节

	printf("%zu\n", sizeof(&arr[0] + 1));
	//因为arr[0]作为二维数组首个子数组的数组名,在用&取地址后,就是这个首个子数组的数组指针,加一后就是第二个子数组的数组指针,所以大小是8字节

	printf("%zu\n", sizeof(*(&arr[0] + 1)));
	//根据上面的解释,&arr[0] + 1是第二个子数组的数组指针,所以经过解引用后就是访问整个子数组,所以大小为16字节

	printf("%zu\n", sizeof(*arr));
	//这里的数组名arr没有单独放到sizeof中,所以代表首个子数组的地址,所以解引用后访问的就是整个子数组,所以大小是16字节。也可以这样理解:*arr中的arr是二维数组首个子数组的指针,所以解引用后访问的就是整个子数组,所以也就是相当于子数组的数组名,所以实际上*arr就相当于arr[0],所以是二维数组第一个子数组的数组名,所以只放一个数组名在sizeof中,返回整个数组的大小,所以是16字节

	printf("%zu\n", sizeof(arr[3]));
	//虽然这里越界访问了,但是对于sizeof,它只关注数据的类型,并不关心内容,所以对于这里的arr[3],我们可以当作arr[0]来分析,所以这里是16字节

	return 0;
}

运行结果:

可以看到与二维数组相关的参数经过 sizeof 运算符后,得到的结果是不尽相同的。

四、数组与指针

下面的程序在32位平台运行会输出什么:

cpp 复制代码
#include <stdio.h>

int main()
{
	int arr[4] = { 1,2,3,4 };
	int* ptr1 = (int*)(&arr + 1);
	int* ptr2 = (int*)((int)arr + 1);
	printf("0x%08x\n0x%08x\n", ptr1[-1], *ptr2);
	return 0;
}

运行结果:

在分析前我们可以先复习一下大小端字节序《C语言------数据存储_c语言数据存储-CSDN博客》。

下面我们来详细分析:

大部分的cpu都是小端字节序,我们这里也是小端字节序,所以数组的存储情况(这里的每一个格代表一个字节):

这里为了方便查看,将内存块简化成了一行。

cpp 复制代码
int* ptr1 = (int*)(&arr + 1);

首先时&arr,这就获得了指向数组的指针了,数值上和数组首元素的指针相同,这里假设数值是0x00ff1200:

然后数组指针加一,由于是数组指针类型,所以加一就会跳过一个数组的步长,所以直接跳到0x00ff1210:

然后这个数组指针就被强制转换为了整型指针,数值上还是这个地址,然后赋给了 ptr1 ,然后在 printf 中的 ptr1[-1] 就相当于 *(ptr1 - 1) 因为 ptr1 是整型指针,所以减一会向前跳一个整型,这时就会指向数组的第四个元素,所以会打印0x0000004。

cpp 复制代码
int* ptr2 = (int*)((int)arr + 1);

这里的 arr 则是代表数组的首元素地址,上面我们假设的是0x00ff1200,然后这个地址就被强制转换成了 int 类型,这里的代码要在32位平台运行的原因就在这,因为32位平台的地址是4字节,int 类型也是4字节,如果是用64位平台,那就要用更大的类型了,转换成 int 类型后,就是整型数据加一,所以单纯是数值上加一,对于指针上,就是跳了一个字节,这时在强制转换成 int* 类型,然后赋值给 ptr2 ,这时 ptr2 的值是0x00ff1201:

然后 ptr2 又是一个整型指针,所以在 printf 中 *ptr2 会访问后面的四个字节的内容,

由于这里是小端字节序,所以 *ptr2 访问到数据后,也会以小端字节序的情况处理数据,所以它会将处理好的,它认为的正常顺序的数据返回回来,也就是0x02000000,

所以会打印0x02000000。

相关推荐
qq_172805598 分钟前
RUST学习教程-安装教程
开发语言·学习·rust·安装
wjs202415 分钟前
MongoDB 更新集合名
开发语言
monkey_meng18 分钟前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
legend_jz43 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
tangliang_cn1 小时前
java入门 自定义springboot starter
java·开发语言·spring boot
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
威威猫的栗子1 小时前
Python Turtle召唤童年:喜羊羊与灰太狼之懒羊羊绘画
开发语言·python
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
bluefox19791 小时前
使用 Oracle.DataAccess.Client 驱动 和 OleDB 调用Oracle 函数的区别
开发语言·c#