一、指针是什么?
1、指针、地址、内存
相信很多同学在学习了指针之后还不清楚指针是什么?
- 对于指针来说,它在内存中其实是中一个最小单元的编号,也就是地址。通俗一些说其实就相当于我们在酒店开了一间房,这个房间的编号就叫做地址,你也可以把它叫做一个指针,那么这间房间就只是这个酒店里面的一个编号而已
- 其实这么看来就可以说==指针、内存、地址==这三者其实是等价的
- 上面说到的是在内存中指针的说法,但是在我们学习C语言时,口头上所称的指针,通常指的是
指针变量
,是用来存放内存地址的变量
【总结一下】:指针就是地址,口语中说的指针通常指的是指针变量
2、指针与变量
- 上面说到了指针变量,那我们就接着这个来做一个展开:当我们去定义出一个变量的时候,其实可以可以使用
[&]取地址操作符
去取出这个变量在内存中的地址,然后存放到一个变量中,那此时这个变量就叫做【指针变量】 - 一起到VS中通过代码来看看
int a = 10; int* pa = &a; printf("%p\n", &a); printf("%p\n", pa);
- 可以看到,通过将变量a的地址取出来给到变量pa,然后以
%p内存地址
的形式去打印【pa】和【&a】的值就可以看到两个值是相等的,就可以说明pa里面确实存放了a的地址 - 在操作符章节的时候我有讲到过如何去看待
int* pa = &a
,对于这个*
来说值得就是pa这个变量是一个指针变量。它的前面的int
表示的就是这个指针变量存放的是一个整型的地址,那它就是一个【整型指针变量】 - 这样看可能会比较抽象,我们可以通过调用【内存】的方式来观察一下
- 此时就可以发现变量a在内存中是占4个字节的,而
[&a]
则是取出了首字节的地址,这么看相信你一定是非常得清晰了
- 不仅如此,除了整型变量的地址可以被存起来,字符型的地址也可以被存放起来,如下
char ch = 'c'; char* pc = &ch; printf("%p\n", &ch); printf("%p\n", pc);
- 那既然这个pc是一个变量的话,操作系统也会在内存中为其分配地址,我们可以去打印这个【指针变量】的地址看看
- 同样,我们可以通过调用【内存】的方式去观察一下,便可以看出char类型的变量在内存中确实只占一个字节
【总结一下】:指针变量,用来存放地址的变量
。(存放在指针中的值都被当成地址处理)
3、解答:为何指针均为4个字节❓
上面我们讲到了一个指针可以存放一个变量的地址,明白了整型和字符型的变量在内存中所占的大小,那指针在内存中占多少空间呢?
- 这里我定义了三个不同类型的变量以及不同类型的指针变量去接收它们的地址,接着使用
sizeof()
去计算了它们各自的地址
int main(void) { char ch = 'c'; int a = 10; float f = 3.14f; char* pc = &ch; int* pa = &a; float* pf = &f; printf("%d\n", sizeof(pc)); printf("%d\n", sizeof(pa)); printf("%d\n", sizeof(pf)); return 0; }
- 从运行结果可以看出每个指针变量的大小均为4个字节,这是为什么呢?这还要从机器中的【地址线】讲起
- 经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)
- 就是0101这样的存储方式,然后根据二进制的逢二进一去罗列出这32根地址线可以存储下多少地址,这里告诉你,一共是有2^32^个地址可以存储
- 每个地址标识一个字节,那我们就可以计算出(2^32^Byte = 4GB) 4G的空间进行编址
这里我们就明白:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储(1B = 8b),所以
一个指针变量的大小就应该是4个字节
- 那如果在64位机器上,如果有64个地址线,那
一个指针变量的大小是8个字节
,才能存放一个地址
- 可以看到,若是我们将编译器放在64位系统上运行,最后的显示结果就为【8】,就可以验证我们上面的说法
【总结一下】:
- 指针变量是用来存放地址的,地址是唯一标识一个内存单元的
- 指针的大小在32位平台是4个字节,在64位平台是8个字节
二、指针的进一步理解
对指针有了初步的一个认识之后,接下去我们来进一步的认识指针有什么用?它存在的意义究竟是什么?
1、指针和指针类型
- 我们都知道,变量有不同的类型,整型,浮点型等。那指针有没有类型呢?
——> 准确的说:有的
- 在上一小节我介绍了指针可以存放其对应数据类型变量的地址,那对于指针本身来说这个类型究竟意味着什么呢?可以看到下面有各种各样不同类型的指针👇
char *pc = NULL; int *pi = NULL; short *ps = NULL; long *pl = NULL; float *pf = NULL; double *pd = NULL;
这里可以看到,指针的定义方式是: [type + *]
- 那有同学一定会想,既然==指针里面存放的都是变量的地址,而且每个指针的大小都是4个字节==。那为何不把指针定义为一个统一的标准呢?就像【宏定义】一样定义出这个指针,完全不需要去考虑它需要存放什么变量类型的地址
- 例如就把指针统一地定义为【ptr】就会非常方便
这一点现在还说不清楚,当你看完指针存在的意义时就会明白这一切了👇
2、指针的解引用
然后先来说说有关指针解引用的问题
- 一样看到下面这段代码,指针pc里面存放的是ch的地址,指针pa里面存放的是a的地址,在上面我只讲到了指针和变量之间地址的关系,但是没有说到
地址和值
的关系 - 那对于一个值来说它是存放在这块地址上的,既然我的这个指针存放了变量的地址,那可不可以访问到这块地址中存放着的内容呢❓
char ch = 'c'; int a = 10; char* pc = &ch; int* pa = &a;
- 此时就可以使用到
[*]
解引用这个操作符了,便可以取到这块地址中所存放的内容,可以看到与其存放的变量中的值都是一样的
printf("ch = %c\n", ch); printf("*pc = %c\n", *pc); printf("a = %d\n", a); printf("*pa = %d\n", *pa);
- 我们知道赋值运算符,若是一个变量的值你不想要了,那就可以修改这个变量的值,那既然指针可以访问到这个变量的值,可以不可以修改呢❓
- 也是一样,可以通过
[*]
解引用的方式就可以做到
*pc = 'd'; *pa = 20;
- 通过运行结果就可以看到里面的值确实做了修改
上面只是带你进一步了解了指针的更多作用,下面我们要真正地深入指针的挖掘,理解指针存在的意义了
⭐指针存在的意义1:访问字节的范围
好,我们来看如何去展示不同类型的指针究竟有什么它们各自存在的意义
- 首先我定义了一个变量a为它放入了一个值,要注意这个值它不是一个地址,而是一个十六进制的值,将其转换为二进制就可以发现刚好为32位,那一个整型变量可存放的数据大小也为4个字节32位
- 然后将这个变量的地址存放到一个指针中去,那这个指针存放的便是一个整型数据的大小,此时它就是一个【整型指针】。然后我通过解引用获取到了这块地址中所存放的内容,现在要去做一个修改
int a = 0x11223344; int* pa = &a; *pa = 0;
- 此时我们就可以通过内存块的变化来看看究竟
解引用
修改值后内存中是如何变化的👈
相信通过上面这幅图一定是非常清晰了
- 但是我为了要验证不同的指针类型究竟有什么不同的意义,所以我便将这个==整型变量的地址存放到一个字符型的指针中去,然后再去修改这个整型变量的值==,看看会发生什么变化
int a = 0x11223344; char* pc = &a; *pc = 0;
- 可以看到,只改变了一个字节,也就是8个比特位的长度,我这里一行显示的4个字节,在内存中是一行显示一个字节的
【总结一下】:==指针类型 决定了指针在进行解引用操作的时候能访问几个字节【权限有多大】==
char*
的指针,解引用访问1个字节int*
的指针,解引用访问4个字节double*
的指针,解引用访问8个字节
⭐指针存在的意义2:类型决定步长
除此之外,不同类型指针存放的意义就是它们移动的步长不一样
- 通过下面的代码可以看到,两个不同类型的指针都接受了整型变量的地址,我们知道指针是可以进行偏移的,那使这两个指针都向后偏移1会发生怎样的变化呢?
int a = 10; int* pa = &a; char* pc = &a; printf("pa = %p\n", pa); printf("pa + 1= %p\n\n", pa + 1); printf("pc = %p\n", pc); printf("pc + 1= %p\n", pc + 1);
- 很明显可以看出,对于整型指针来说,
+1
会向后偏移4个字节;而对于字符型指针来说+1
会向后偏移1个字节。这其实就可以看出不同指针其实还是有着它们的不同意义
【总结一下】:==指针类型 决定了指针的步长(向前 / 向后走一步都多大距离) ==
char*
的指针 + 1【跳过一个字符型,也就是向后走1个字节】short*
的指针 + 1【跳过一个短整型,也就是向后走2个字节】int*
的指针 + 1【跳过一个整型,也就是向后走4个字节】double*
的指针 + 1【跳过一个浮点型,也就是向后走8个字节】
小练习:初始化数组
通过了解了上面有关不同指针类型的概念之后,相信你对指针一定能够有了一个自己的理解,接下去我们来做一个习题练练手
- 现在我初始化了一个数组,大小为10,首先对其所有元素初始化为0。拿一个指针变量去接收一下这个数组的首元素的地址,这样来看就可以通过这个指针访问到这个数组了
int arr[10] = { 0 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < sz; ++i) { *p = i + 1; p++; }
- 通过一个循环去遍历这个数组的大小,然后使用
*
解引用便可以访问到当前循环遍历的那个元素,就可以利用循环变量做一个初始化了,然后p++
每次让指针向后偏移一个元素,便可以初始化完所有的数组元素了
相信我这么说你还有点懵,没关系,可以通过画图来分析一下
- 首先看到指针p指向数组的首元素地址,此时使用
*p
便可以访问到这块地址上的元素,即可以做修改,此时【i = 0】,i + 1
便是1,所以这块地址中的内容会被初始化为1,接着p++
,接着p++
跳过4个字节的大小,因为它是一个整型指针,便来到arr[1]
这块空间的地址
- 同理,来到数组第二个元素的地址后,依旧可以通过
*p
访问到这块地址中的内容做一个修改,然后指针p会向后移动4个字节的大小,刚好跳过一个数组元素,因为这是一个整型数组,数组中的每一个元素都是整型的
- 最后,当【i】遍历完了整个数组的大小之后也通过这个指针初始化完了数组的所有元素,此时指针也移动到了数组最后一个元素地址
- 来看一下运行结果
- 当然,为了简洁方便,我们也可以将初始化元素和指针的后移动同时进行,也就是下面这种写法
for (int i = 0; i < sz; ++i) { *(p + i) = i + 1; }
p + i
访问到了让当前数组位置所在地址,然后通过*
解引用便访问到了这块地址所在内容,然后一样的【i + 1】便可以进行一个修改,将其放在循环里就可以使得每次i
的值在变化的同时带动指针的偏移,最后也是可以完成数组的初始化