C语言基础:(十六)深入理解指针(6)
目录
前言
一、sizeof和strlen的对比
1.1 sizeof
1.2 strlen
1.3 sizeof和strlen的对比
二、数组和指针笔试题解析
2.1 一维数组
2.2 字符数组
例题一:
例题二:
例题三:
例题四:
例题五:
例题六:
2.3 二维数组
三、指针运算笔试题解析
3.1 题目一
3.2 题目二
3.2.1 数据在内存中的对齐方式
3.2.2 结构体Test的内存布局
3.2.3 指针运算分析
(1)p + 0x1
(2)(unsigned long)p + 0x1
(3)(unsigned int*)p + 0x1
3.3 题目三
3.4 题目四
总结
前言
本期是C语言基础中有关指针的最后一篇博客,本期将为大家介绍sizeof和strlen的对比、数组和指针乃至指针运算的笔试题解析。那么现在让我们开始吧!
一、sizeof和strlen的对比
1.1 sizeof
在学习操作符的时候,我们学习了sizeof,sizeof是用来计算变量所占内存空间的大小的,单位是字节,如果操作数是类型的话,计算的就是适用类型创建的变量所占内存空间的大小。
sizeof只关注占用内存空间的大小,不在乎内存中存放了什么数据。
比如:
#inculde <stdio.h>
int main()
{int a = 10;printf("%d\n", sizeof(a));printf("%d\n", sizeof a);printf("%d\n", sizeof(int));return 0;
}
1.2 strlen
strlen是C语言中的一个库函数,其功能是求字符串长度。函数原型如下:
size_t strlen ( const char * str );
该函数统计的是从strlen函数的参数str这个指针指向的地址开始向后,一直到 '\0' 之前的字符串中字符的个数。
strlen函数会一直向后找 '\0' 字符,直到找到为止,所以可能存在越界查找的情况。
strlen函数的演示如下所示:
#include <stdio.h>
int main()
{char arr1[3] = {'a', 'b', 'c'};char arr2[] = "abc";printf("%d\n", strlen(arr1));printf("%d\n", strlen(arr2));printf("%d\n", sizeof(arr1));printf("%d\n", sizeof(arr2));return 0;
}
1.3 sizeof和strlen的对比
sizeof | strlen |
1. sizeof是操作符 | 1. strlen是库函数,使用需要包含头文件string.h |
2. sizeof计算操作数所占内存的大小,单位是字节 | 2. strlen是求字符串长度的,统计的是 \0 之前字符的个数 |
3. 不关注内存中存放什么数据 | 3. 关注内存中是否有 \0,如果没有 \0,就会持续向后找,可能会越界。 |
二、数组和指针笔试题解析
首先,我们来回顾一下数组名有何意义:
- sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
- &数组名,这里的数组表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址。
2.1 一维数组
我们来通过下面这些例题,学会判断sizeof函数内的表示的是整个数组还是首元素地址:
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
分析如下:
2.2 字符数组
同样让我们来先看一些例题:
例题一:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
例题一分析如下:
例题二:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
例题二分析如下:
例题三:
分析输出结果。
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
例题三分析如下:
例题四:
分析代码结果。
char arr[] = "abcdef";
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
例题四分析如下:
例题五:
分析输出结果:
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
例题五分析结果如下:
例题六:
分析输出结果:
char *p = "abcdef";
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
例题六分析结果如下:
2.3 二维数组
分析下列代码的输出结果:
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));
分析结果如下:
三、指针运算笔试题解析
3.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表示的是数组首元素的地址,那么a+1就表示在a的基础上跳过一个元素,即指向数组中的第二个元素,故*(a+1)的值为数组中的第二个元素2;由于&a表示整个数组的地址,故&a+1就表示跳过整个数组,所以*ptr就是a数组最后一个元素之后的地址,故ptr-1指向的就是a数组的最后一个元素,因此*(ptr-1)的值为5。输出结果如下图所示:
3.2 题目二
//在X86环境下
//假设结构体的⼤⼩是20个字节
//程序输出的结果是啥?
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;
}
3.2.1 数据在内存中的对齐方式
在计算机系统中,对齐(Alignment) 是指数据在内存中的存储位置必须满足特定边界要求,以提高访问效率。不同的数据类型有不同的对齐规则,这会影响结构体的大小和内存布局。
结构体的对齐方式受 最严格成员的对齐要求 影响,并可能插入 填充字节(Padding) 来满足对齐。规则总结如下:
-
结构体的对齐 = 最大成员的对齐值
例如,如果结构体包含int
(4)、char
(1)、double
(8),则结构体按8
字节对齐。 -
每个成员的偏移量必须是其对齐值的整数倍
如果int
(4 字节对齐)放在偏移2
,编译器会插入 2 字节填充,使其移动到4
。 -
结构体末尾可能需要填充,以保证数组对齐
如果结构体大小不是对齐值的整数倍,编译器会在末尾填充,使得sizeof(struct)
是最大对齐值的整数倍。
3.2.2 结构体Test的内存布局
在x86环境下,默认是4字节对齐(32位系统),此时结构体Test的成员大小和对齐情况如下:
因此Test的总大小=4(Num)+ 2(sDate)+ 2(cha)+ 8(sBa)= 20字节
3.2.3 指针运算分析
(1)p + 0x1
p是一个struct Test* 指针,其初始值为0x100000。在C语言中,指针运算的单位是指向类型的大小。
由于p的类型是struct Test*,且sizeof(struct Test)= 20,因此p+0x1相当于p + 1,即向后移动1个struct Test的大小,计算过程如下所示:
0x100000 + 1 * 20 = 0x100000 + 0x14 = 0x100014
最后输出的值为:0x100014
(2)(unsigned long)p + 0x1
(unsigned long)p将指针p强制转换为了unsigned int*(指向unsigned int 的指针)。我们又可以知道,在x86环境下,sizeof(unsigned int)的值为4,所以 (unsigned int*)p + 0x1就相当于向后移动1个unsigned int 的大小,计算如下:
0x100000 + 1 * 4 = 0x100004
最终输出的值为:0x100001
(3)(unsigned int*)p + 0x1
这一步的计算与上一步同理,最后可得计算过程如下:
0x100000 + 1 * 4 = 0x100004
最后的输出结果为:0x100004
因此,最后整个代码的输出结果为:
0x100014 // p + 0x1(结构体指针运算)
0x100001 // (unsigned long)p + 0x1(纯数值运算)
0x100004 // (unsigned int*)p + 0x1(整型指针运算)
3.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;
}
//输出结果是什么?上述代码有什么问题?
首先,代码中给二维数组a赋值的部分存在错误,像(0,1)这样的表达式并不是数组初始化的语法,而是逗号表达式。在C语言中,逗号表达式(x,y)的值是y,即最后一个表达式的值;因此(0,1)的值是1,(2,3)的值是3,(4,5)的值是5.
受逗号表达式的影响,数组a的实际初始化方式相当于:
int a[3][2] = { 1, 3, 5 }; // 其余部分默认补 0
即:
a[0][0] = 1
a[0][1] = 3
a[1][0] = 5
a[1][1] = 0
a[2][0] = 0
a[2][1] = 0
再来看指针p的赋值部分,a[0]是二维数组a的第一行,其类型为int[2],但会退化为 int*。而p指向了a[0][0],即 p = &a[0][0]。又由于我们已知a[0][0] = 1,因此p[0]的值就是1。最后程序输出的结果就是1。
正确的初始化方式如下:
int a[3][2] = { {0, 1}, {2, 3}, {4, 5} }; // 正确写法
即应该将[ ] 替换为{ },此时程序输出的结果为0。
3.4 题目四
#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;
}
aa是一个 2x5
的二维数组,初始化如下:
int aa[2][5] = {{1, 2, 3, 4, 5}, // aa[0]{6, 7, 8, 9, 10} // aa[1]
};
内存布局如下(假设 int 占 4 字节):
地址 值
&aa[0][0] 1
&aa[0][1] 2
&aa[0][2] 3
&aa[0][3] 4
&aa[0][4] 5
&aa[1][0] 6
&aa[1][1] 7
&aa[1][2] 8
&aa[1][3] 9
&aa[1][4] 10
对于ptr1, &aa 是 整个二维数组的地址,类型是 int(*)[2][5]
(指向 2x5
数组的指针)。&aa + 1
会跳过整个aa,即 + sizeof(aa) = +40字节
(2x5x4
)。强制转换为 int*
后,ptr1 指向 aa
的末尾(即 &aa[1][4] + 1
)。ptr1 指向 aa
的末尾,ptr1 - 1回退 1
个 int (4 字节)。所以 *(ptr1 - 1)的值
就是 aa[1][4]
,即 10。
aa 是二维数组名,类型是 int [2][5]
,但会退化为 int (*)[5]
(指向第一行的指针)。aa + 1
指向第二行 aa[1]
,即 &aa[1]
。*(aa + 1)
解引用得到 aa[1]
,类型是 int[5]
,但退化为 int*
(指向 aa[1][0]
)。所以 ptr2
指向 aa[1][0]
(即 6
)。ptr2
指向 aa[1][0]
,ptr2 - 1
回退 1
个 int(4 字节)。所以 *(ptr2 - 1)
就是 aa[0][4]
,即 5
。
因此本题最后的输出结果为:10,5
总结
C语言指针部分的内容到本期就正式结束啦!相信读完我的文章的大家应该能有所收获,那么也还请继续关注博主,C语言的学习之路还没有结束哦~~~