C 语言中,指针存储的是变量的内存地址!!!
要彻底理解指针,首先要理解 C 语言中变量的存储本质,也就内存。
内存编址与内存空间
计算机的内存是一块用于存储数据的空间,由一系列连续的存储单元组成。
如下图所示,每个单元格都表示 1 个 bit,8 个 bit 为一组,称为 byte;byte 是计算机中作为内存寻址的最小单元。
1 byte = 8 bit
可以理解为,计算机给每个 byte 一个编号,这个编号就是内存的地址。
计算机中,每个 byte 的编号都是唯一的,从而保证了计算机可以通过每个编号访问到唯一确定的 byte。
因为内存中,每个 byte 都有一个唯一的编号,所以编号的范围也就决定了计算机中可以寻址的内存空间。
所有编号连起来就是内存的地址空间。
变量存放
C++ 或 C 中可以如下定义向量
int a = 999;
char c = 'c';
当定义一个变量时,实际上是向内存申请了一块空间来存放变量(不同的数据类型,申请的空间是不同的)
int 类型占 4 个字节,在计算机中使用补码保存。
999 的补码为:0000 0011 1110 0111
按照大端的存储方式,上述变量 a 会在内存中,如上图方式保存;如果是按照小端的存储方式,会如下图方式保存
大端:高位字节放在内存的低地址的存储方式
小端:低位字节放在内存的低地址的存储方式
对于 float 、 char 、数组、指针、结构体、对象等类型的数据实际上也是一样的,需要先转换为补码,再按照大端或者小端的方式,依次将字节写入到内存单元中
指针
定义一个变量实际上就是向计算机申请了一块内存存放数据,这个地址可以通过运算符 &
得到(虚拟内存地址,并不是实际物理地址),得到的值就是变量所占内存块的起始地址,在 C 语言中通过 指针 这一概念来表示这个地址。
int * pa = &a;
pa 中存储的就是变量 a 的地址,也称为指向 a 的指针
所以,指针的本质,就是变量的内存首地址,即一个 int 类型的整数
那么问题来了,如果指针都是一个 int
类型的整数,为什么会有各种数据类型的指针呢?
pa
中存储的是变量 a
的内存地址,通过这个指针地址得到 a
的值的操作,称为 解引用, *pa
由于指针存储的只是变量内存的首地址,对于不同变量,其所占内存空间是不同的,如果不能把所有数据从内存空间取出,那么解引用得到的数据就会出错,而指针的类型,则可以帮助编译器判断,从首地址开始取多少字节的数据,从而保证解引用的正确
pa
指针本身也是一个变量,在内存中占有一定的空间
案例
float f = 1.0;
short c = *(short*)&f;
从内存层面而言,f
没有发生任何变化
第二行代码,只是把 f
的前两个 byte 取出来,然后按照 short
指针的方式解引用,最后赋值给 c
那么,对于下面两行代码呢?
short c = 1;
float f = *(float*)&c;
(float*)&c
会让我们从 c
的首地址开始取四个字节,然后按照 float
的编码方式去解释
但是 c 语言 中 short
类型只占两个字节,那肯定会访问到相邻后面两个字节,这时候就发生了内存访问越界。
当然,如果只是读,大概率是没问题的,但是,如果向这个区域写入新值
*(float*)&c = 1.0;
那么就可能发生 coredump,也就是访存失败
另外,就算是不会 coredump,这种也会破坏这块内存原有的值,因为很可能这是是其它变量的内存空间,而我们去覆盖了人家的内容,肯定会导致隐藏的 bug。
多级指针
多级指针,就是指针的指针的指针的指针…
实际上,多级指针只是一个为了方便我们理解和表达的逻辑概念
同样一个内存,如果存放的是别的变量的地址,就是指针;如果存放的是实际内容,就是变量;当存放的是别的指针的地址,就是指针的指针,依此类推,可以得到任意多级指针
指针本身也是一个变量,需要内存取存储,指针也有自己的地址
指针内存存储的是它所指向变量的地址
int a;
int *pa = &a; // 指针
int **ppa = &pa; // 二级指针
int ***pppa = &ppa; // 三级指针
如上图所示,pppa
就是三级指针,ppa
就是二级指针,pa
就是指针
对于 int ** a
可以把它分为两部分看,int *
和 *a
,*a
表示 a
是一个指针变量,int*
则表示 a
存放的是 int *
型变量的地址,对于其他多级指针都可以如此类推