一、一维数组与 sizeof
对于 sizeof 的参数,输入不同的与数组相关的参数,sizeof 返回的结果是不同的,下面给出一些例子与分析:
#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 函数返回的值是不尽相同的:
#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 函数返回的值是不尽相同的。
一些错误示范:
#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* 类型的,而这里错误示范的前两个传的参数是字符型数据,我们可以进行详细分析:
printf("%zu\n", strlen(*arr));
这里的 arr 代表首元素地址,解引用后就是首元素,而首元素是字符 a ,所以传给 strlen 的参数是字符 a ,而字符 a 的 ASIIC 码值是97,所以这里 strlen 就将对97这个地址进行访问,并试图在这个地址上查找一个字符串,这导致了未定义行为。对于97这个地址,这个地址在大多数操作系统上是不合法的或未分配给你的程序的,因此尝试访问它导致了访问违规错误或段错误。
后面的两个错误示范都是传了整个数组的地址,类型是 char(*)[6],与 const char* 这个类型实不符的,所以这里的参数也是错误的。
三、二维数组与 sizeof
对于 sizeof 的参数,输入不同的与二维数组相关的参数,sizeof 返回的结果是不同的,下面给出一些例子与分析:
#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位平台运行会输出什么:
#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都是小端字节序,我们这里也是小端字节序,所以数组的存储情况(这里的每一个格代表一个字节):
这里为了方便查看,将内存块简化成了一行。
int* ptr1 = (int*)(&arr + 1);
首先时&arr,这就获得了指向数组的指针了,数值上和数组首元素的指针相同,这里假设数值是0x00ff1200:
然后数组指针加一,由于是数组指针类型,所以加一就会跳过一个数组的步长,所以直接跳到0x00ff1210:
然后这个数组指针就被强制转换为了整型指针,数值上还是这个地址,然后赋给了 ptr1 ,然后在 printf 中的 ptr1[-1] 就相当于 *(ptr1 - 1) 因为 ptr1 是整型指针,所以减一会向前跳一个整型,这时就会指向数组的第四个元素,所以会打印0x0000004。
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。