赌你会懵的C语言指针进阶数组场景解析
正片开始
细化指针这一部分内容,现在着重把一些指针的运用情景搬出来康康,如果对指针盘的不是非常熟练,或者指针还出于入门阶段的铁子请绕道(晕头警告)
直接给大家盘个套餐:
一维数组
int a[] = {1,2,3,4,5}; 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));
问题很简单,这组 printf 的值是多少?小朋友你是否有些许害怕,但是没有关系我们逐个击破,这是后续内容的基础
记住你的答案,来看看编译器是怎么解析的:
首先我们应该知道数组名代表的是数组首元素的地址,但是要知道我们有两种例外:
1.sizeof (),()内是数组名时,代表的就是整个数组,计算的就是数组的大小,单位是字节;
2. & 数组名时,表示的也是整个数组,取出的就是整个数组地址;
除了以上两种情况,其余的所有数组名都表示首元素地址!
所以
1.sizeof(a)
就是直接将数组名放进去,算出的就应该是 sizeof(int)*5 = 20
2.sizeof(a+0)
此时不只有数组名,因此 a 代表首元素地址,a+0 等价于 a,是地址大小,在32位/64位平台下对应 4/8 字节大小
3.*a
,不只有数组名 a 代表首元素地址,解引用得到首元素,sizeof(a)= sizeof(int)= 4
4.a+1
老规矩还是首元素地址+1,就是第二个元素地址,是地址大小为 4/8 字节
5.a[ 1 ]
,大小为4
6.&a
为整个数组地址,但是地址终归是 4/8 字节
7. * &a
,&a
是类型为 int()[4] 的数组指针,解引用数组地址为整个数组,大小 20
8. &a +1
取出整个数组地址,一个数组指针的单位就是整个数组,+1 就会跳过整个数组,大小还是为第二个数组地址,大小 4/8
9. &a[0]
,首元素地址,4/8
10. &a[0] + 1
,第二个元素地址,4/8
字符数组
char a[] = {'a','b','c','d','e','f'}; 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)); printf("%d\n",sizeof(&a+1)); printf("%d\n",sizeof(&a[0]+1));
和上面同理,再来看看编译器怎么解答的:
1.a
是数组名,首元素地址计算整个数组大小为 6
2.a + 0
,首元素地址 + 0 还是首元素地址,4/8
3.*a
,首元素大小 ,1
4.1
5.&数组名为整个数组地址,4/8
6.跳过一个数组,下一个数组地址,4/8
7.第二个元素地址,4/8
这么简单?nonono,这才刚刚开始
char a[] = {'a','b','c','d','e','f'}; printf("%d\n",strlen(a)); printf("%d\n",strlen(a+0)); printf("%d\n",strlen(*a)); printf("%d\n",strlen(a[1])); printf("%d\n",strlen(&a)); printf("%d\n",strlen(&a+1)); printf("%d\n",strlen(&a[0]+1));
我还是先把运行结果啪出来吧:
诶?怪诶,打印7下出来2个?nnd给我玩阴的是吧。这里首先强调一下,sizeof 是操作符,海纳百川来啥算啥,‘ \0 ’也会照收,而 strlen 是傲娇的库函数,傲娇在于strlen 针对的是 \0 之前的字符串长度(个数),而且不包含 ‘ \0 ’。
1.a 数组名,没有在 sizeof 内部,为首元素地址,我们知道 strlen 在遇到 \0 之前是不会停下来的,字符数组我们没有给到 \0就是没有,因此什么时候遇到他我们不知道,起码会比当前数组大,不知道嘛时候停就是个随机值
2.同理,随机值
3.首元素地址解引用为首元素 ‘a’,strlen 需要的是一个地址,此时会从把‘a’ 默认为一个地址,a的ASCII码值为 97,就会从内存中地址为 97 的地方开始往后数字符个数,但是注意 97 不属于我原本分配的内容,属于非法访问内存,直接报错崩溃垮掉
4.同理,报错
5.数组的地址,虽然和 strlen 参数类型有所差异,但还是从第一个位置向后数,那就是随机值啦
6.跳过一个数组的地址,依然是随机值
7.第二个元素地址,依然依然是随机值
那么就又双可以联想到字符串了,引入指针变量进行讨论:
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)); 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));
直接看结果吧,这下就感觉出有点意义不明了,结果对应哪个数据都不知道了,所以这时候自主分析的价值就出来了
- p是一个指针,sizeof 算指针的大小,4/8字节
- p+1,就是 p 的值加1,字符指针p是一个地址,数值上+1是 b 的地址,所以大小 4/8字节
- p 为char* 的指针,解引用访问首元素,大小为 1
- 小问号你是否有很多朋友,指针为啥还可以 [0] ?其实 p[0] 等价于 *(p+0),p是首元素地址,p+0亦是,解引用出来就是首元素,1
- p是地址,&p是对地址取地址,也就是二级指针,是地址那就是 4/8
- 跳过一个char*的地址即跳过了一个p的地址,本质上还是地址,4/8
- p[0]为首元素,取地址+1 为第二个元素的地址,4/8
1.在 “abcdef”中是隐含了一个‘ \0 ’的,所以传入p算出大小为 6
2.同理,从第二个元素开始,5
3.解引用为a,97,报错
4,等价于*p,就是首元素,报错
5,取数组地址的地址往后数字符串,为随机值
6.+1跳过一个地址的地址往后数,依然是随机值
7.第二个元素的地址往后数,随机值,等价于 p+1
二维数组
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]));
看一下运行结果:
1.a数组名,为整个数组,124 = 48
2.数组首元素,14 = 4
3.数组第一行元素,44 = 16
4.a[0]此时为首元素地址,即a[0]|0] 地址,+1为 a[0]|1] 地址,4/8
5.等价于a[0] [1],,14 = 4
6.数组名不是单独存放为第一行地址,+1表示第二行地址,4/8
7.解引用为第二行元素,44 = 16
8.a[0]为第一行地址,&a[0]+1为第二行地址,4/8
9.解引用为第二行元素,44 = 16
10.解引用首元素地址为第一行,44 = 16
11.淦!好怪!数组只有3行却搞出个 a[3],这不越界了吗?其实编译器会很聪明的自行推算,按照已给数组排列就是 44 = 16
整点硬菜
热身完了,来整点题目,让你混乱的大脑脉动回来(雪上加霜)
1.
int main() { int a[6] = {1,2,3,4,5,6}; int *ptr = (*int)(&a+1); printf("%d %d",*(a + 1),*(ptr - 1)); return 0; }
ptr 是int类型指针,&a是数组地址,+1跳过一个数组,指向数组末位的地址,强转为 int* 类型,*(a+1)首元素地址+1再解引用得到第二个元素 ,ptr - 1指向 5 的地址,所以答案为 2,5。
2.
//假设stu 大小为 20 字节 struct stu { char* name; int age; float score; } *p; int main() { (struct stu*)p =0x100000; printf("%p\n",p + 0x1); printf("%p\n",(unsigned long)p + 0x1); printf("%p\n",(unsigned int*)p + 0x1); return 0; }
p是一个指针类型指向结构体,代表结构体的地址, p + 0x1 就是地址数值上进行 + 0x1 操作,p = 0x100000 ,我们知道 int* +1 跳过4个字节,char* + 1跳过1 个字节,我们这里是结构体类型,就跳过 20 个字节,20 化成16进制相加就是 0x100014
p 强转为 unsigned long 其实就是整型,p 就是一个纯纯的数字了,那0x100000 既然已经是“数字”了,那就有了可以直接加减的属性,就是 0x100001
p强转成无符号整型指针类型, 一个指针类型是 4 个字节,结果就是 0x100004
到这里是不是有内味儿了,那咱继续
3.
int main() { int a[4] = {1,2,3,4}; int* ptr = (int*)(&a+1); int* ptr2 = (int*)((int)a+1); printf("%x %x",ptr[-1],*ptr2); return 0; }
此题请仔细思考,大坑警告!
ptr 为整型指针,&a为整个数组地址,+1跳过整个数组来到末位的地址,再强转成 int * 类型(这里大可不必,因为它本来就是一个 int * 指针)ptr[-1] 等价于 * (ptr - 1),再代入 ptr 就代表最末地址 -1 来到 4 的地址,ptr[-1 ]就是 4。
ptr2中 a为首元地址,这里注意优先级问题,先强转成 int 就是个纯纯的数字可以直接进行 +1 操作,即地址进行了数值+1,格局高不高就要看下一步,接下来需要考虑 大小端问题,因为我们是小端存储,按照低位字节在低地址处,所以 1,2,3,4 在内存中是这样的:
…… | 01 | 00 | 00 | 00 | 02 | 00 | 00 | 00 | 03 | 00 | ……
假设 01 地址是 0x01,int 强转后 a +1 偏移一个字节,指向的就是 01 后面一个 00 的位置,所以 ptr2 就指向这个位置,我们解引用拿到他的值就是整型大小的值就是 00 00 00 02,再以小端的形式拿出来就是 20 00 00 00。
4.
int main() { int a[3][2] = {(0,1),(2,3),(4,5)}; int* p; p = a[0]; printf("%d",p[0]); return 0; }
p[0] 等价于*(p+0),p = a[0], 就是*a[0] ,a[0] 是首元素地址,那么问题来了:是 0 的地址吗?
请仔细看看我们二维数组的逗号表达式,其实整个数组就只有 3 个元素:1,3 5;他们在数组中排列出来是:
1 | 3
5 | 0
0 | 0
所以解引用出来就是 1。
今天就到这儿吧,摸了家人们