很喜欢一部动漫,《星游记》,有这样一句话:“管你什么乱起八糟的,我们只是要,往前走啊!”
来看一下我们这次要抵达的彼岸吧!
一:内存和地址
二:指针变量和地址
三:指针变量类型的意义
四:const修饰指针
五:指针运算
六:野指针
七:assert断言
1.内存和地址
在深入理解指针前我给大家引入一个生活中的案例:某地呢!有一栋宿舍楼,该宿舍楼最大特点是,没有房间号也没有楼层号,这时同学a进入一个房间,b同学要找a同学玩,那么b同学只能随机访问每个房间,不能很快找到a同学,那如果给楼层和房间标上对应的楼层号和房号,那么b同学能够直接能够锁定a的位置,找其玩耍!
将上面案例对应到计算机中,CPU访问数据要去内存中读取,那么内存如何高效管理呢!这里把内存划分成了一个一个的小的内存单元,并且规定一个内存单元的大小必须是8bit(比特位)也就是1byte(字节),目前CPU仍然不能直接迅速的访问到想要的数据(这里类比上面的案例),所以我们要为内存单元进行对应且唯一的编号标识,这样CPU就能够像同学b一样迅速找到对应的房间(内存单元)!c语言标准对内存单元的编号进行了规定,编号 = 地址 = 指针。
Bit(比特位) | 8Bit = 1Byte |
Byte(字节) | 1024Byte = 1KB |
KB | 1024KB = 1MB |
MB | 1024MB = 1GB |
GB | 1024GB = 1TB |
补充一点:1bit可以存放二进制位中的一个1或者一个0。
我们接着对上面引出来的地址进行一个扩展学习,地址如何进行编址!首先CPU向内存访问数据是硬件和硬件之间的交互,那么想让硬件和硬件建立起联系,则将硬件用线(主要关注地址线)进行连接,这时CPU就可以和内存进行交互了。例如:32位操作系统有32根地址线,通电后地址线将电信号转化为数字信号就对应有高电平1和低电平0,一根地址线有2^1种序列,俩根地址线有2^2种序列,那32根地址线有2^32种序列,这里每一种序列代表一个地址。
补充一点:32位操作系统管理权限是2^32个内存单元,但并不代表内存一定是4GB。
再给大家通过图解方式理解上面内容!如图一;
![](https://img-blog.csdnimg.cn/direct/71639780f81f460da69cbff86b659ff0.png)
2.指针变量和地址
创建一个变量本质是什么?
1. int a = 10; |
解释:变量a在内存中开辟了4个内存单元(int 4字节),用来存放数值10。 |
2.1 取地址操作符(&)
明白了创建变量的本质后,引入取地址操作符(&),使用该操作符可以拿到一个变量的地址;现将a变量进行取地址操作(&a),则取出的地址是4个内存单元中地址最小的内存单元的地址!如图二。
![](https://img-blog.csdnimg.cn/direct/6c45862699fb40f7bd9d6b374f147808.png)
2.2 指针变量
取地址操作符+变量名,可以拿到一个变量的地址,再将该地址进行存放方便后期使用,那该地址存放在哪里合适呢?答案是:指针变量!
指针变量是专门用来存放地址的,反过来说,存放在指针变量里的数据都会被指针变量认为是地址,下面对指针变量的形式展开解释!
1. int a = 10; int *p = &a; |
解释:将a变量的地址取出,存放在指针变量p中。 |
2. int *p = &a; |
解释:int *p = &a;语句中 int 和 * 的意义! int:代表指针变量p指向的是一个数据类型为int的空间。 * :代表p是指针变量。 |
再给大家以图解的方式理一下逻辑!如图三!
2.3 解引用操作符
既然我们将指针变量进行了存放,目的就是为了有朝一日使用该地址。如何使用呢?是通过解引用操作符(*)对指针进行解引用,即可找到指针所指向的空间,进而操作这块空间。
1. int a = 10; int * p = &a; *p = 20; |
解释:*p:通过指针变量p所存放的地址,间接找到指针变量p所指向的a空 间,并且将这块空间的内容改为20; 本质上: *p 等价于 a , a = 20; |
2.4 指针变量的大小
既然已经知道指针变量里存放的是地址,那指针变量的大小也可以推算出来;在32位操作系统下,地址总线大概包含有32根地址线,上电后由电信号转化为数字信号1或0,那么32根地址线就有2^32中序列,每一种序列代表一个地址,每一种序列都是由32个1或0组成,所以32位操作系统下指针变量要想存放地址就需要开辟4个字节的内存空间,64位操作系统皆是如此!总结如下!
32位操作系统 | int大小为4字节 | 指针变量大小为4字节 |
64位操作系统 | int大小为4字节 | 指针变量大小为8字节 |
再给大家贴一张图证明上面的推理没错,如图四!
![](https://img-blog.csdnimg.cn/direct/62825b649fba4e15ae574b5f1cf5e750.png)
大家有没有这样想过,指针变量的数据类型虽然发生变化,但在内存中开辟的空间是不会改变的,那么指针变量花里胡哨的数据类型也没啥用吧!针对大家的想法和好奇在标题三中会为之解答!
补充:为什么不管是32位操作系统还是64位操作系统,int的大小不会发生变化呢?
1. 兼容性:因为大量的软件都假设int的大小是4字节,所以64位操作系统也保持了int的大小为4字 节,这样软件就可以在64位操作系统上无需修改就可正常运行。
2.性能上:使用更小的数据类型通常可以提高性能,因为数据类型小可以有效利用CPU的缓冲,同 时也减少了内存的占用和数据传输时间。
三:指针变量类型的意义
3.1 指针解引用的大小
不必关心内存1中变量a内容的存放顺序,若想了解可参考数据在内存中的存放。
前面我们拆解了指针变量数据类型的含义,int*中的int代表指针变量p认为自己指向的是一个数据类型为int的对象,所以再进行*p的时候指针变量p就可以访问4个字节的大小。
同理char*中的char代表指针变量p认为自己指向的是一个数据类型为char的对象,所以再进行*p的时候指针变量p就可以访问1个字节的大小。
总结:指针变量数据类型的不同,会使指针解引用时访问权限发生变化。
指针数据类型:char* | 解引用访问权限:1个字节 |
指针数据类型:short* | 解引用访问权限:2个字节 |
指针数据类型:int* | 解引用访问权限:4个字节 |
3.2 指针+-整数
指针变量pa骨子里认为自己指向的是char类型的对象,所以pa+1就向后走一个字节的距离,同理pb+1向后走四个字节的距离,减法亦是如此!
总结:指针+-整数,可使指针向前或者向后走一步跳过多大距离。
补充:对一个普通变量直接取地址,则该指针的数据类型是:普通变量类型 + *;
3.3 void*(任意类型指针)
顾名思义任意类型的指针可以接受任意类型的地址,该指针的兼容性很好,可以根据自己的意愿对void*指针进行强制转换再去使用,主要应用在后面的泛型编程中;void*指针的缺点是不能进行解引用和加减运算,因为该指针的数据类型是任意类型,是不确定的。
四:const修饰指针
我们在创建一个普通变量后是可以修改内容的,但如果用const加以修饰,那么该变量再无法对其进行修改。
#include<stdio.h>
int main()
{
int a = 10;
a = 20;
const int b = 10;
//b = 20; //报错
return 0;
}
原因是:变量被const修饰后,使其具有了常属性,但本质仍为变量,这里给大家证明一下。
#include<stdio.h>
int main()
{
const int a = 10;
int arr[a] = {0};
//vs环境下数组的大小必须是常量,而这里用const修饰的变量会报错。
return 0;
}
上面通过const限制了a变量,但如果我们获取到了a变量的地址,就可以间接的去操控a的内存空间。
#include<stdio.h>
int main()
{
const int a = 10;
int * pa = &a;
*pa = 20;
//通过指针修改了a变量的内存空间。
return 0;
}
走到这里思考一下,既然用const修饰了该变量,目的就是不让变量的内容再被修改,而这里很明显违背了最初代码的用意,所以呢!我们就要用const关键字去修饰指针变量。
第一种修饰方式:const关键字修饰在指针类型的*号左边:指针变量不能再进行解引用,进而无法修改指针指向的内存空间,但可以修改指针变量所存放的内容。
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
const int * pa = &a;
//const也可以写成 int const * pa = &a;
//*pa = 20;//报错
pa = &b;
return 0;
}
第二种修饰方式:const关键字修饰在指针类型的*号右边:指针变量能再进行解引用,进而可以修改指针指向的内存空间,但不可以修改指针变量所存放的内容。
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int * const pa = &a;
*pa = 20;
//pa = &b;//报错
return 0;
}