目录
前言:
谈及指针,大部分人只有一个感觉:太难学了,好抽象啊!但是,请不要着急,今天当你看完这一篇后,相信你肯定能够理解什么是指针了。
1、内存和地址
1.1 理解内存和地址
在介绍指针前,我们需要先了解什么是内存和地址。
关于内存和地址,生活中有一个例子可以很好的解释它们
比如说你住在一栋宿舍楼,大楼内有100个房间,但是房间并没有编号。这时,你的一个朋友来找你玩,如果想找到你,就得一个房间一个房间的寻找,这样效率很低。但是,如果我根据楼层和楼层的房间的情况,给每一个房间都编上号,比如:
1楼:101 102 103……
当有了门牌号,这时候你只需要将门牌号告诉你朋友,他就可以很快速的找到房间,找到房间里的你。
如何将上面的例子抽象到计算机里呢?你可以理解宿舍楼就是内存,房间就是内存中的一个内存单元,房间里的你就是数据,而门牌号就是地址。
所以内存就是存储数据的空间
我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何高效的管理呢?
cpu从内存中读取数据,就好比是你朋友要在宿舍楼里找到你,而你朋友找你,也只是在一个个房间寻找。大楼里的一个一个房间的划分,让人们对大楼内的面积能够充分利用,而计算机中也是如此。内存的空间只有8GB/16GB/32GB,因此对于内存的合理运用也变的很重要。
计算机中把内存也划分为一个个的内存单元,每个内存单元的大小取1字节。
(补充)计算机中常见的单位:
一个bit可以存储一个2进制位的1和0
地址(门牌号) | 内存(大楼) | 其中,每个内存单元,相当于⼀个学⽣宿舍,一 个字节空间里面能放8个比特位,就好比同学们住 的八⼈间,每个人是⼀个比特位。 | |||
0xFFFFFFFF(16进制) | 1个字节 | ||||
0xFFFFFFFE | 1个字节 | ||||
1个字节 | 每个内存单元也都有⼀个编号(这个编号就相当 | ||||
(内存单元) | |||||
生活中我们把门牌号也叫地址,在计算机中我们 把内存单元的编号也称为地址, | |||||
1个字节 | |||||
0x00000001 | 1个字节 | ||||
0x00000000 | 1个字节 |
所以我们可以理解为:
内存单元的编号 == 地址 == 指针
1.2 理解编址
生活中关于我们可以看到通过宿舍门上的门牌号,直接找到我们想去的地方。门牌号是真实存在与宿舍门上的。而内存中的地址我们该怎么理解呢?
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
这就像钢琴,吉他上面没有刻上“剁、来、咪、发、 唆、拉、西”这样的信息,但演奏者照样能够准 确找到每⼀个琴弦的每⼀个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且 所有的演奏者都知道。本质是⼀种约定出来的共识!
有点抽象,举个例子:
图书馆里有一排排的书架,每个书架又有一层层的格子,这些格子就好比内存中的存储单元。
给每个格子编上号,也就是编址,就像是要给图书馆里的每个格子都贴上标签。
那这个标签是怎么贴上去的呢?这就得靠图书馆的“硬件设计”了。
比如说,书架的排列方式、格子的划分规则,就像是硬件的设计。
想象一下,书架是固定的,它们的位置和大小决定了格子的位置和数量,这就好比硬件决定了内存有多少个可以存储数据的地方。
然后,有一套专门的标记系统,就像特殊的机器或者装置,按照书架和格子的排列,给每个格子都印上编号,这就是通过硬件实现了编址。
那硬件设计又是怎么实现的呢?
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协 同,至少相互之间要能够进⾏数据传递。 但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。 ⽽CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。通过地址总线,我们就可以了解什么是硬件编址了。
32位机器有32根地址总线, 每根线只有两态,表示0,1【电脉冲有无】,那么⼀根线,就能表示2种含义,2根线就表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。 地址信息被下达给内存,在内存上,就可以找到 该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
内存编址这件事,是靠计算机里面那些实实在在的硬件设备,按照一定的规则和办法来做好的。
简单来说,计算机的编址是通过硬件设计把每一个内存单元的地址都固定好了,不需要把地址额外的存起来。
知识补充:
- 32位机器有32根地址总线,64位机器有64根地址总线。
- 地址总线是实际存在的物理电线
2、指针变量和地址
当我们了解了内存和地址的关系后,就可以开始对指针的学习啦!
在C语言中变量创建的本质是向内存申请空间
比如说:int a = 10;
这串代码就相当于向内存内申请了4个字节,一个整型占4个字节,这串空间我们想存放的数据便是变量10。
2.1 取地址操作符:&
int main() { int a = 0x11223344;//16进制数字 return 0; }
16进制0x11223344,一个16进制位可以改写为4个二进制位,因此,11223344可以改写为32个二进制位表示,刚好一个整型可以放下。
我们可以调试来看一下内存:
打开后,输入&a 并敲下回车键,列改为一行(记得在x86环境下观察,比较方便)
我们可以看到44,33,22,11都各占一个字节,每一个字节都有一个地址。
我们将列数改为4列再观察
从上面我们可以看到a确实向内存申请了4个空间。
读到这我们可能会有一个新的问题,欸,4个字节都有地址,那我们怎么知道a的地址是哪一个呢?
还记得前面调试的时候我们是怎么观察地址的吗?我们通过输入&a按下回车后出现了0x00E2FEC4,因此0x00E2FEC4便是a的地址,我们也可以发现这个地址和 44 所占字节的地址一样。
总结一下,&a取出的是a所占4个字节中地址较小的字节的地址
虽然整型变量占用4个字节,但是我们只要知道了第一个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
代码展示一下,怎么打印地址
int main() { int a = 0x11223344; printf("&a=%p\n", &a); return 0; }
知识补充:
- &是取地址操作符,想要得到地址,就需要使用这个操作符
- %p:用来打印地址的占位符
结果展示:
和前面调试的结果不是一样,是因为在打印的时候又申请了新的空间。
2.2 指针变量
在我们之前学习的过程中,我们如果想要将一个整数存储起来,就会创建一个整型变量,比如我们想存储数字10,如下代码:
int main() { int a = 10; return 0; }
我们通过创建了一个整型变量 a 来存储10。
那么如果我们想要将我们通过&得到的地址存储起来,有没有什么办法呢?我们可以将地址存储在指针变量中。比如我们想要存放 n 的地址
int main() { int a = 10; int * pn = &n; return 0; }
解读:int * pn = &n;
1、pn被称为指针变量
为什么呢?
&n——n的地址——地址就是指针
pn = &n;
pn就是用来存放地址的,也可以说是用来存放指针的
指针变量就是存放指针的变量。
可以通过和整型变量来理解指针变量,
整型变量:a就是用来存放整数的。
2、int * 被称为指针类型
int a = 10;
int * pn = &n;
对比就可以发现,int是整数的类型,int*是指针的类型。
2.3 如何拆解指针类型
上文说到了之指针的类型是(int *),那么我们该如何理解指针类型呢?
pn的类型是int *,我们需要分别理解
- *:说明pn是指针变量
- int:说明pn指向的对象是int类型
char ch = 'x';
如果我们想要存放x的地址该怎么写呢?
char * pc = &ch;
这样我们就存放了x的地址
* 告诉我们pc是指针
char则告诉我们指针指向的对象是char类型。
2.4 解引用操作符(*)
当我们将地址保存起来后,是为了后面能够使用,那么我们该怎么使用呢?
在现实生活中,我们使用地址要找到⼀个房间,在房间里可以拿去或者存放物品。
C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这里必须学习⼀个操作符叫解引用操作符(*)。
int main() { int n = 10; int * pn = &n; //解引用操作符(间接访问操作符) *pn = 100; printf("%d", n); return 0; }
上述代码中的*pn就使用了解引用操作符, *pn 的意思就是通过pn中存放的地址,找到指向的空间, *pn其实就是n变量了;所以*pn=100,这个操作符是把n改成了100.
从结果上看,n的值的确被改为了100
或许通过这个例子你会觉得指针有是什么用?如果只是想修改n的值,为什么不直接写一个n=100呢?这样不是更方便吗?
其实这里是把n的修改交给了pn来操作,这样对n的修改,就多了⼀种的途径,写代码就会更加灵活, 后期慢慢就能理解了。
其实这里有一个很好的例子能说明指针的作用,生活中,有些事情是不方便自己去做的,因此呢,就需要委托别人来代替你做,比如说一个老板想要喝奶茶,但是他不会自己顶着大太阳出去买,而是会吩咐他的秘书取帮他完成,差不多就是这样,可能有些不恰当,见谅哈!
2.5 指针变量的大小
我们知道我们创建一个整型变量int的大小是4个字节,字符变量char的大小是1个字节,那么指针变量的大小又是多少呢?
思考过程:
指针变量存放的是地址,地址的存放需要多大的空间呢?知道地址存放的空间就是指针变量的大小
也就是说指针变量的大小取决与地址的大小
通过前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后 是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变的大小就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个二进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变量的大小就是8个字节。
int main() { int n = 10; int * pn = &n; printf("%zd\n", sizeof(pn)); return 0; }
在32位机器上: 可以看到打印的大小是4个字节
在64位机器上: 可以看到打印的大小是8个字节
那么指针类型是否会影响指针变量的大小呢?我们来测试一下
int main() { printf("%zd\n", sizeof(int*));//整型 printf("%zd\n", sizeof(char*));//字符 printf("%zd\n", sizeof(short*));//短整型 printf("%zd\n", sizeof(double*));//双精度浮点型 return 0; }
在32位系统上结果:
在64位系统上结果
我们可以看到,不管指针类型是什么,都不会影响指针变量的大小,指针类型的变量大小,在相同平台下,大小都是相同的。
总结:
- 32位平台下地址是32个bit位,指针变量大小是4个字节
- 64位平台下地址是64个bit位,指针变量大小是8个字节 X64环境输出结果
- 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
3、指针变量类型的意义
既然指针变量大小与指针类型无关,那么为什么还要搞指针的变量类型呢?
3.1 指针的解引用
int main() { int n = 0x11223344; int* pi = &n; *pi = 0; return 0; }
调试过程:(注意观察内存里的值)
运行到292行时内存展示44 33 22 11
当经过*pi = 0 之后
内存里4个字节全部变为了 0
如果我们不用int *的指针类型,改为char *的类型,结果又是如何呢?
我们可以看到只是将n的第⼀个字节改为0。
我们可以看到int类型指针可以访问4个字节,而char类型指针只访问了1个字节。
通过对比,我们可以得到一个结论:
结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。
3.2 指针+-整数
int main() { int n = 0x11223344; int* pi = &n; char* pc = &n; printf("&n = %p\n", &n); printf("pi = %p\n", pi); printf("pi+1 = %p\n", pi+1); printf("pc = %p\n", pc); printf("pc+1= %p\n", pc+1); return 0; }
结果:
我们可以发现, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
4、void* 指针
void的意思是无,或者空
所以void*指针是无具体为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
int main() { int n = 10; char* pc = &n; return 0; }
在上面的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。而是用void*类型就不会有这样的问题。
使用void*类型的指针接收地址就不会出现警告
void* 类型的指针不能直接进行指针的+-整数和解引用的运算
int main() { int n = 10; void* pc = &n; *pc = 20; return 0; }
当我们想运行的时候,就会报下面这个错误
void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算。
这是因为void*是一个无具体类型的指针,当进行解运算的时候,没法确定访问几个字节。
既然如此,那void*有什么作用呢?
专门用来存放别人传送过来的地址,当你不知道别人给你传的是什么类型的指针的时候,就可以使用void*来存放,当需要进行解运算的时候,在使用强制类型转换来实现。
比如:
int main() { int n = 10; void* pc = &n; *(int*)pc = 20; return 0; }
这样就将类型强制转换成了整型指针,结果就可以打印出来了
结语:
本篇文章主要讲了指针的基本知识,通过本篇文章能够了解什么是指针,指针变量,指针类型是什么。后面会继续更新指针相关知识,希望能够帮助大家攻克指针这一模块。