❤引言❤
本篇文章配上了大量的图解,必将为君斩断指针这一梦魇!
🍕第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是首元素地址,类型为
int*
(一维数组的数组名是首元素地址)。 -
&a是取到整个数组的地址,类型为
int (*)[5]
(&数组名拿到整个数组的地址)。
&a拿到了整个数组的地址,(&a + 1)
会跳过整个数组,为了将(&a + 1)
赋给ptr,它将其强制转换为int*
的类型。
ptr-1
则会倒退一步,回到数组的最后一个元素,如图示:
a是数组首元素地址,a+1
则会跳过一个元素,如图示:
🍔第2️⃣题
假设p 的值为0x100000。 如下表表达式的值分别为多少?
(已知,结构体Test类型的变量大小是20个字节)
//第二题
#include <stdio.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
int main()
{
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
解析:这道题实际考察的是指针+1(指针的类型决定了+1的步长),0x1
中0x代表是16进制,那么0x1
也就是表示十六进制的1。
p+0x1:p的类型为(struct Test*)
,所以p+1会跳过一个结构体的大小(20个字节)
16
+
4
=
20
16+4=20
16+4=20,故0x00100014
就是0x00100000
加上20个字节后的十六进制序列。(前面两个0是我们自己补上去的,在32位平台上,一般十六进制形式都表示成8个十六进制位,因为一个十六进制位表示成二进制位最多为四个1(1111
),那么两个十六进制位就是8个二进制位(1个字节),8个十六进制位表示的恰好是32个二进制序列。)
(unsigned long)p+0x1:p的类型并不是指针是整数,那么整数+1就是正常的+1即可。(不论二进制/十进制/十六进制
,它们只是一个数值表现的不同形式,它们的进位不同而已,无论哪种形式,正常+1即可)
(unsigned int*)p+0x1:p被强制转换为整型指针,整型指针+1跳过一个整型大小(4个字节)
🍟第3️⃣题
//第三题
#include <stdio.h>
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}
解析:
-
ptr[-1]
可以改为*(ptr-1)
-
%x以无符号十六进制的形式打印。
-
&a是取到整个数组的地址,类型为
int (*)[4]
(&数组名拿到整个数组的地址)。
(int*)(&a + 1):&a拿到整个数组的地址,+1则跳过整个数组,为了将(&a + 1)
赋给ptr1,它将其强制转换为int*
的类型。ptr1[-1]
等同于*(ptr1 - 1)
,则倒退一步,回到数组的最后一个元素,如图示:
(int*)( (int)a + 1 ):a是首元素的地址(地址是一个十六进制序列),(int)a
也就是将十六进制强制转换为了一个十进制的整数,(int)a+1
也就是整数+1,然后再将其转换为int*
类型(也就是十六进制的数或者说以十六进制形式表现的地址),因为之前将其转换为十进制整数+1后又转换为地址,故十六进制的数值(地址)也必然是+1之后的数值,而十六进制(地址)+1就是跳过了一个字节。(一个字节分发一个地址,地址+1跳过一个字节,且地址一般表示为十六进制形式,在32位系统下十六进制一般表现为8位,俩位表示一个字节。)
注:代码中最小是int型(4个字节),而跳过一个字节则需要详细了解大于每个字节在内存中存放顺序的问题▶《多字节数据在内存中存放顺序之字节序》)
图解:图为十六进制序列形式(俩位为一个字节)
🌭第4️⃣题
//第四题
#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;
}
解析:
-
逗号表达式从左到右依次计算并取最后一个值
此题二维数组实际上存放的数据是:int a[3][2] = { 1, 3, 5 };
,每行存放两个元素,后续补0。a[0]
拿到第一行的一维数组地址,将其赋给p之后,p则相当于第一行一维数组的数组名,p[0]
则是第一行一维数组的首元素。
🍿第5️⃣题
//第五题
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = (int(*)[4])a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
解析:
-
二维数组的数组名是一维数组的地址。
-
指针的
*
和[]
可以相互转换(p[4] = *(p+4)
)
此题,数组名a是第一行的地址,其类型为int(*)[5]
,指向对象类型为int [5]
,故其+1跳过一个5个元素的数组。p是数组指针,其指向一个有四个元素的数组,类型为int(*)[4]
,指向对象类型为int [4]
,故其+1跳过一个4个元素的数组。
图示,我们已经找到了两个的位置,那么指针-指针就是它们之间的元素个数,这里因为是低地址处指针减后面的,故为:-4。而以%p的形式打印-4,地址一般以十六进制形式显示,这里也就是打印该数的十六进制形式。
- 计算机内存中只存储补码
- %d是打印原码
- %p是以地址的形式打印,地址一般是十六进制,但是地址不会分原反补,而是直接拿计算机内存储的进行打印。
故我们先求出计算机内存储的补码,再将计算机内存储的二进制序列转换为十六进制序列:
10000000 00000000 00000000 00000100
原码
11111111 11111111 11111111 11111011
反码
11111111 11111111 11111111 11111100
补码
ff ff ff fc
十六进制
故%p打印出来的结果为:fffffffc
🧇第6️⃣题
//第六题
#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是一维数组的地址,类型为
int(*)[5]
(二维数组的数组名是一维数组的地址)。 -
&aa取到整个数组的地址,类型为
int(*)[2][5]
(&数组名拿到整个数组的地址)。
&aa
的类型为int(*)[2][5]
,&aa+1会导致跳过int(*)[2][5]
的长度(也就是整个数组),而这时会直接跳到刚好越界的位置,因为(&aa+1)
的类型被强制转化为int*
则指针步长被转换为4个字节,-1时则会回到数组的最后一个元素。如图示:
aa的类型为int(*)[5]
,(aa+1)
会导致跳int(*)[5]
的长度(也就是一个五个元素的一维数组),*(aa + 1)
相当于aa[1]
(二维数组第二行首地址),同理-1时会回到前面一个元素(第一行的最后一个元素)。如图示:
🧀第7️⃣题
//第七题
#include <stdio.h>
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
解析:我们学过基础的存放字符串的方法(字符数组和字符指针),可是每次只能存放一串,那我们想要存放多串字符串呢?这里就用到了字符指针数组(数组的每个元素都是一个字符指针,每个字符指针存放每个字符串的首字符地址,本质上就是用每个字符串首字符的地址初始化数组)。
char* a[0]
存放的是字符串“work”首字符’w’的地址。(类似于char* p = "work";
)char* a[1]
存放的是字符串“at”首字符’a’的地址。(类似于char* p = "at";
)char* a[2]
存放的是字符串“alibaba”首字符’a’的地址。(类似于char* p = "alibaba";
)
因为a是首元素地址,这个数组的首元素是指针,所以a就是指针的地址,也就是二级指针(指针数组的数组名是二级指针)。所以a也就可以赋给一个二级指针来指向它。
-
char**
(pa)解引用拿到char*
(a[0])的内容,char*
(a[0])内存放的是’W’的地址。 -
pa++
因为pa指向的类型是char*
,所以pa++
跳过一个char*
。
🥪第8️⃣题
//第八题
#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;
}
解析:
-
指针自增或自减将会永远改变自身的指向
-
指针是变量,变量就会有自己的地址,变量就会存放数据,指针变量内存放的是普通变量地址或者下一级指针的地址。
我们先来看一下这段代码刚开始时候的内存布局图:
注:因为数组数组名表示首元素地址,故小标c+0/cp+1
都是表示那一块空间的地址
一、 **++cpp
:自增优先级高于解引用,并且此处前缀自增,故先自增再解引用:
1、
++cpp
;cpp+1,cpp指针向后走一步。因为cpp的类型为char***
,它指向类型是char**
类型的对象,故此,cpp+1会跳过一个char**类型的变量。cpp指向的对象改变了,则cpp内存放的地址随之改变,变为cp+1。如下图:
2、
* ++cpp
;解引用cpp指向的对象(char**),则拿到其指向对象空间内存放的东西,这里存放的是地址(类型为char**的变量内部存储的地址同样是char**,就是char*的地址),也就是下图框红的空间:
3、
**++cpp
;继续解引用char**,则得到char*,char*内存放的是char的地址,在这里也就是’P’的地址。
二、 *-- * ++cpp + 3
:按优先级,+3是留到最后运算;而cpp指针起始状态应该是上一次移动后的状态,因为上一段代码使用了自增运算符永久改变了指针指向。
char***
解引用拿到其指向的对象char**
,char**
解引用拿到其指向的对象char*
,char*
解引用拿到其指向的对象char
。1、
* ++cpp
;先自加再解引用一次,三级指针(char***)解引用拿到其指向的二级指针(char**)。也就是下图我们框红的空间:
2、
-- * ++cpp
;将拿到的char**类型的指针变量自减,这个变量空间的地址是cp+2(char***),里面存放的是地址是c+1,自减后也就是c。当地址为cp+2的这块空间内存放的地址改变后,显然其指向也会随之改变。注:这里的解引用后-1并不是cp这个指针减一,而是指针c,cp的类型是char***,而
* ++cpp
之后的对象是char**。
3、
*-- * ++cpp
;将改变指向后的二级指针(char**)解引用,拿到其指向的一级指针(char*),下图框红的空间:
4、
*-- * ++cpp + 3
;最后将解引用拿到的一级指针+3,char*类型指向的对象为char类型,故跳过3个char类型数据,如下图:
三、 *cpp[-2] + 3
:此处可以将形式改为*(*(cpp-2))+3
;因为上面都用到自增自减所以指针指向被改变,故保持上述状态继续。
1、
*(cpp-2)
;cpp类型为char***,其指向对象为char**,cpp-2则回跳2个char**类型,再将其解引用则拿到cpp-2指向的空间,这块空间地址为cp+0,类型为char**:
2、
*(*(cpp-2))
;再解引用拿到char**指向的对象char*的空间:
3、
*(*(cpp-2))+3
;char*类型+3则加上3个char*类型指向的对象(char):
四、 cpp[-1][-1] + 1
:此处可以将形式改为*(*(cpp-1)-1)+1
1、
*(cpp-1)
;cpp类型为char***,cpp指向对象为char**类型,故cpp-1回跳一个char**类型,后解引用拿到其空间:
2、
*(*(cpp-1)-1)
;拿到地址为cp+1里面存放着c+2地址的这块空间,将其-1则c+2-1得到c-1,指针存放的地址改变了,其指向随之改变,再解引用拿到其空间:
3、
*(*(cpp-1)-1)+1
;char*类型的指针内存放着字符’E’的地址,其+1,则跳过1个char*指向的对象char。(或者说,空间存放着’E’的地址其地址+1得到后面的字符地址):
-
指针是一个变量,操作系统会在内存中给它开辟相应的空间,它的空间内存放的是其他变量的地址。
-
指针变量若是自增自减则会改变内部存放的地址,指针变量空间内存放的地址如果改变,指针的指向也就随之改变。(此操作会永久的改变指针的指向)我们一般不会操作直接操作指针指向而是间接操作。
指针变量的加减,只有自增自减时才会直接改变内部存放的地址(一般是在外部/间接
p+1
,而不是在内部/直接p++
;即使是自增p++;
其实也就是p = p + 1;
)。而指针加减的步长取决于这个指针指向的对象
- 举个例子:
char a[] = "ABCDE"; char** pa = a;
- 如果pa+1,则pa会跳过一个其指向对象的类型的空间大小(pa指向的对象类型为
char*
,是一个指针变量,32位平台大小为4个字节大小)因为一个字节空间分配一个地址,所以在地址上是以这个指针存放的地址增加4。- 如果a+1,则a会跳过一个其指向对象的类型的空间大小(a指向的对象类型为
char
,是一个字符型变量,1个字节大小)因为一个字节空间分配一个地址,所以在地址上是以这个指针存放的地址增加1。
- 指针-指针
笔者百思不得其解,指针-指针就是指针内部存放的地址相减,而一个字节的空间会被分配一个地址,那么地址-地址应该就是两个地址之间字节个数的差值,拿这两个地址相减也确切得出是它们之间字节个数的差值,可是当笔者在VS2019上以%d的形式打印时,显示的却是两个地址之间元素个数的差值。后来在翻阅《C和指针》一书时,才得其所以然,原来的确,指针-指针算出的就是它们之间字节个数的差值,但是它在此之后编译器将其除以数组中元素所占的字节个数,最后打印出来的结果也就是指针之间元素的个数。
详细可查阅《C和指针》第六章P109
❤对于指针运算的感悟:
指针一定会按一个类型所占字节空间大小为单位来移动,而此操作在其内部存放的地址上除却char类型,基本上都以大于1的值来增减。
所以指针无论怎样加减都会跳过其指向对象的类型所占空间的大小,在地址上,一个字节空间分配一个地址,故跳过相应几个字节空间,地址上便增减相应的值。而这里的地址指的是指针指向的地址(指针变量存储的地址)而不是指针变量自己的地址。若不进行相应的操作,指针变量自己的地址是不会改变的。若非要改变,则需要用到二级指针。
❤对于一级指针与二级指针关系的理解:
二级指针改变一级指针的指向
(全剧终)感谢食用!
|
|
|
往期回顾:
【详解浮点型在内存中的存储】
【字节序之Little-Endian&Big-Endian】
【自己写了一个万能排序】