🏖️作者:@malloc不出对象
⛺专栏:《初识C语言》
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
目录
- 前言
- 一、指针是什么
- 1.1 如何理解编址
- 二、为什么要有指针/地址
- 三、指针变量是什么
- 四、指针与指针变量的区别
- 五、指针的内存布局
- 六、指针解引用
- 6.1 int* p = NULL与*p = NULL的区别
- 6.2 如何将数值存储到指定的内存地址
前言
本篇文章我将带大家初步认识指针,在C语言当中无疑最重要也是比较难的一部分就是指针,在进行数据结构的学习章节当中指针用的也是相当之多,只有对指针掌握的相当熟练了才能学起来得心用手。那么本文的任务呢,今天是先带大家了解一下指针是什么?指针是用来干什么的…本篇文章注重理论部分,相当于给后续的技术文章做一个铺垫。
一、指针是什么
在回答这个问题之前我想带大家重新理解一下变量,下面请看一个例子:
请问你是如何看待下面代码的变量a?
#include<stdio.h>
int main()
{
int a = 0;
a = 10;
int b = a;
return 0;
}
虽然这是一段很简单的代码,但是实际上我们自己一定要清楚的知道变量a在各种场景中含义,下面我带大家分析一下。
第五行代码是在干嘛呢?它是对变量a进行初始化,变量a与生俱来拥有一块空间并且空间里面放的内容为0.
a = 10,它进行的是赋值操作,它使用的是a的空间,将10放入这块空间,此时我们的a充当的是左值.
int b = a,将变量a中的内容赋值给b,使用的是a的内容,此时我们的a充当的是右值.
由此,我们可以得出一个小结论:同样一个a变量,在不同的应用场景中,a本身的含义是不同的。我们不要肤浅的认为a就是一个变量名称,一定要深刻理解其中充当的含义。
接下来我们来重新认识一下变量:
定义一个变量,本质是在内存中根据类型来进行开辟空间。有了空间,就必须具有地址来标识空间来方便CPU进行寻址。有了空间,就可以把数据保存起来。
什么是指针?
指针就是地址,地址就是指针,指针就是内存中一个最小的单元编号。
但就我们平时大多数使用的情况来说,这里我想说明一点此"指针"非指针,具体原因看下文慢慢分析。
既然上文说了指针(地址)是内存中一个最小的单元编号,那么它是如何标识一个内存单元的呢?内存中最小的单元又是什么呢?我们来看下面一幅图:
那么从图中我们可以看到内存最小的存储单元为一个字节,那么为什么不是其他单元呢,下图已经证实了,画的太丑大伙儿将就看吧orz~
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0).
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
下面我们来分别验证一下在32/64位环境下指针(地址)的大小(字节)?
x86环境下:
x64环境下:
由此我们也可得出一个结论:指针的大小在32位平台是4个字节,在64位平台是8个字节。它们的大小是固定的,指针(地址)的大小只跟操作环境有关系。
之前有小伙伴问过我这一个问题?
一个字节有一个地址,而一个地址却要占4个字节,这个该如何理解?一个地址所占的字节比它对应的字节还要大?!
我们应该这么来理解:一个字节一个地址,每一个字节都被分配一个地址,而不是一个字节等于一个地址!!!
好了,那么我又有个问题了,既然内存中的最小存储单元是一个字节,那么请问一个众多字节的变量,取出的是哪个字节对应的标识地址呢?又或者是其他处理方式呢?
例如:一个int类型四个字节的变量,&这个变量得到的是四个地址?很显然我们平时&某个变量得到的只有一个地址是吧。
那么请问它取出的是这个变量四个字节当中哪个字节的地址呢?为何只需要得到一个字节的地址就能正确的将a表示出来呢?
这些问题我们先保留着,到下面我会跟大家解释清楚,读者可以先思考一番,如果这些问题你都很清楚的话,那么这篇文章不是为你准备的🙈🙈
1.1 如何理解编址
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递。但是硬件与硬件之间是互相独立的,那么如何通信呢?
答案很简单,用"线"连起来。而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。
不过,我们今天关心一组线,叫做地址总线。CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址(就如同宿舍很多,需要给宿舍编号一样)计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
钢琴吉他上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识!
硬件编址也是如此,我们可以简单理解, 32位机器有32根地址总线,每根线只有两态,表示0 , 1电脉冲有无,那么一根线,就能表示2种含义, 2根线就能表示4种含义,依次类推。 32根地址线,就能表示2^32中含义,每一种含义都代表一个地址。地址信息被下达给内存,在内存内部,就可以找到改地址对应的数据,将数据在通过数据总线传入CPU内寄存器.
二、为什么要有指针/地址
我们说指针是什么?指针就是地址,那为什么存在地址呢?
这是为了更好的定位,加快你的寻址速度。不然的话你只能盲目的进行寻址,这样的效率极低而且还不一定能达到目的。
举个例子:生活中为什么会有门牌号?是不是为了方便寻找,有了门牌号我们可以直接根据这个地址快速找到对应的内容。
因为有了指针(地址)大大提高了查找效率!!!而具有指向性的数字我们称之为指针。
类比计算机,CPU在内存中寻址的基本单位是多大?在32位机器下,最多能够识别多大的物理内存?
既然CPU寻址按照字节寻址,但是内存又很大,所以,内存可以看做众多字节的集合。其中,每个内存字节空间,相当于一个学生宿舍,字节空间里面能放8个比特位 ,就好比同学们住的八人间,每个人是一个比特位。每间宿舍都有门牌号就等价于每个字节空间对应的地址,即该空间对应的指针。
三、指针变量是什么
谈到指针部分我最想提的其实是指针变量,那么我们先联系一下变量的概念,地址是数据,那么数据可不可以被保存在变量空间里面呢?当然是可以的,所以就引申出了指针变量
这一概念。
指针变量是什么?
保存指针(地址)数据的变量就叫做指针变量。它是一个变量,既然是变量那么它也有地址来标识它所对应的空间,这里顺便提一嘴保存当前指针变量的地址就要用二级指针变量来接收了,,这是后话了下篇文章我们再来谈论这个问题。
四、指针与指针变量的区别
为什么我在开头提过此"指针"非指针这句话呢?
严格意义上,指针和指针变量是不同的,指针就是地址值,而指针变量是C中的变量,既然是变量那么就会在特定的区域开辟空间,用来保存地址数据,它也有自己的地址。
在验证指针与指针变量的区别之前,我先带大家大致的了解一下指针变量是如何使用的,大家看向下面一段程序:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
return 0;
}
怎样方便理解指针?
这里说一下我个人对于理解指针的小tip:int* p = &a;这里的p它的类型其实是int*型而非int型,为什么很多初学者把它认为是int型呢,因为往往他们是这样来写的int *p = &a,虽然左边部分也可以这样来写,但是我就是个人推荐哈如果你对指针不熟练的话,以后定义指针变量时最好把变量名单独分开,这样它的类型就一目了然了,type + 变量名。这个*在这里起到的作用是什么呢?起到的作用就是表明p是一个指针变量,它所指向的对象的数据类型为int型,后续我们还会谈到*它的解引用操作。
好了我们初步了解了指针变量的用法之后,接下来我们就来谈谈指针与指针变量的关系:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a; //指针变量p保存着a的地址,相当于初始化操作
p = (int)0x1234; //将0x1234放到p所在空间的内容中,充当左值提供空间,相当于赋值操作
int* q = p; //此时p充当右值,提供的是内容,可以理解为就是将0x1234赋值给q,当指针变量作为右值时,可以认为指针 == 指针变量
return 0;
}
我在代码中做了注释相信大家都能明白,这里再提一点不要把指针变量想的太过于复杂,它其实就跟普通变量一样,只不过指针变量的内容保存的是地址(指针)罢了。这里我再简单对上述代码做一个关系图:
前面2行代码的内存图示如下,以下地址纯属瞎编只是简单做个分析
此时p的内容是a的地址,通过解引用p能找到a:
后俩行p此时作为左值提供的是空间将0x1234赋给p,此时对p解引用其实是已经找不到a了,因为p的内容被修改了啊,并且现在对p进行解引用会导致程序崩溃,因为此时0x1234这个地址你无法知道这个地址的内容你有权限能够进行访问;int* q = p对q进行初始化,此时p作为右值提供内容,此时p的内容即地址,而地址即指针,p又是一个指针变量,所以此时可以认为指针 == 指针变量。
结论:指针就是地址,指针变量是一个变量,变量内部保存指针(地址)数据,它也有自己的地址,并且在我们平时听到的指针其实大多是指针变量,当指针变量作为右值时才可以认为指针 == 指针变量。
我们在口语化表达的时候经常讲的是指针,我个人认为可能是比较顺口又或者是可能与最早期的C语言资料的翻译有关,在后来的很多书籍上也是将指针与指针变量这俩者的概念混淆了。
那么我们以后怎么认为呢?
首先指针这个叫法顺应大多数人的习惯,后续我也是采用指针的叫法,但是我们在心中谈到指针时一定分析清楚这到底是指针还是指针变量。
五、指针的内存布局
这部分我将对文章开头部分的诸多问题进行一一解答,接着我们来看看下面的一个简单例子:
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
print("%d\n",*p);
return 0;
}
这段代码非常的简单,对于初学指针的读者来说也许你们的老师或者很多视频都说到过理解指针最好的方法是画图,下面我简单的画一下指向图:
我们已经知道&a取出的只是一个地址,那么这个地址到底是a的哪个字节的地址呢?如何全部访问这4个字节呢?
首先我们分析取到的地址只可能会是俩个端点的地址,即首字节或者尾字节的地址,为什么呢?
我们常说有始有终,要想得到a数据那么a的四个字节全部都要被访问,那么怎么才能达到全部访问的目的呢?
我们首先取得a的首字节的地址,而a的数据在逻辑地址上是连续的,所以通过找到首地址我们就能找到a变量其范围内的其他字节。
口说无凭,接下来我们再次进行验证:
我们发现了什么?&a得到的是首字节的地址,即0x44的地址(起始地址),我们的指针变量p接收的也是a的起始地址,也许有读者有疑惑为什么44是首字节呢?因为VS下采用的是小端模式,低地址在低位,高地址在高位,如果还不懂的话可以看我的这篇博客哟~
最终我们可以得出一个结论:在C语言当中,所有变量取地址都是取的该变量众多字节当中对应地址最小的那个。更通俗的来说,就是找到变量的起始地址了就能访问到该变量范围内其他字节址了。
这里我再谈一下我个人的理解,其实无论要访问什么数据都是要得到它的起始地址才能访问到其他范围内对应的数据,当然了这是建立在这个地址的内容我们有权限去访问的前提下。
顺便提一嘴,数据元素的访问也是建立在知道数组起始地址的前提下的,例如:arr[5],其实我们是根据数组首地址来直接进行访问的:*(arr + 5),而不是我们简单的就是看着数组下标就知道对应的元素。关于数组与指针的关系我不久会专门出一篇文章来阐述它们之间的关系的,这里就不做过多的对比了。
六、指针解引用
指针解引用这个部分充分的体现了指针变量的作用,通过变量的地址找到其对应的内容,解引用就相当于一把钥匙。
下面我们来看看指针解引用的例子,请读者思考一下8、9行代码的*p到底表示的是什么含义?
#include<stdio.h>
int main()
{
int a = 10;
int* p = &a;
int b = *p;
*p = 20;
return 0;
}
这里对*p完整理解是,取出p中的地址,访问该地址指向的内存单元(空间或者内容)(其实通过指针变量访问本质是一种间接寻址的方式,这点以后会讲到)。在int b = *p中*p充当的是右值提供内容;*p = 20中,*p充当的是左值提供的是空间,将20放到p所保存地址的内容的中,这一步其实已经把a的值间接改变了。
总结起来,我们其实可以得出这样一个简单的结论: 在同类型情况下对指针解引用代表指针所指向的目标。所以*p就是a,这样理解起来代码就更加一目了然了。
对于上述问题接下来我还想问一个问题?
int a = 10;
int* p = &a;
此时是一个操作符,那么*p就相当于一个表达式,此时p使用的是指针变量p的左值还是右值???
注意我问的是使用指针变量p的左值还是右值。
下面我们来看俩组例子:
#include<stdio.h>
int main()
{
*(double*)0 = 10.0;
return 0;
}
当我们进行调试时程序已经崩溃
来看下一个例子,同样的也是因为访问权限而崩溃:
我们来分析一下,double* p = NULL是将0号地址赋给指针变量p,而下一步p = 10.0是将10.0写入0号地址处,注意了此时是写入0号地址时出现了访问冲突,而指针变量p的内容不就是0号地址吗?
所以此时的p使用的其实就是指针变量p的内容即右值;同样的*(double*)0 = 10.0,其实本质上跟它是一样的将10.0写入0号地址时发生了访问冲突导致程序崩溃。
这俩者本质上出现访问冲突的问题都是由于直接通过地址数值对变量进行访问而造成的,因为此时的0号地址我们并不知道它是不是属于这个程序的地址,有可能是操作系统级别的那么我们就没有权限对它进行访问。
这也就对应说明了上面得出的结论:在同类型情况下对指针解引用,代表指针所指向的目标。
对于上述代码,它们都是直接通过0号地址数据对变量进行访问的,0号地址即指针变量的右值即内容。所以严格意义上来讲,*p使用的是p的右值,即内容。
6.1 int* p = NULL与*p = NULL的区别
int* p = NULL代表的是将0号地址保存在指针变量p中,此时*的作用是表明p是一个指针变量,它所指向的数据类型为int.
*p = NULL代表的是将指针变量p所指向目标地址的内容置为0,此时的*是解引用操作,将0写入目标地址的内容.
注意:NULL、0、‘\0’在数字层面上都是0,但在类型层面上不同,含义不同。
NULL = (void*)0,’\0’的ASCII码值等于0,它是一个转义字符,0就是一个数值0.
6.2 如何将数值存储到指定的内存地址
知道了指针的本质就是地址,地址就是数据,那么我们可以直接通过地址数据对变量进行访问吗?
目前主流的编译器和操作系统,为了安全,已经有了很多内存保护的机制。我们目前的win和Linux都有栈随机化这样的机制来方式黑客对用户数据地址进行预测。当然,还有其他的栈保护机制,比如“金丝雀”技术之类的。当然地址不一定是在栈上才是随机的,在全局数据区也有是随机的,只不过相对较稳定写,这些读者可以自行去检测。
经过试验,目前vs和Centos7上,使用C语言定义的局部变量,在每次运行的时候,地址都是不同的。经过试验发现,定义全局变量,每次更改代码,地址也会发生变化。所以这个实验我们没法正确做出来,但是程序崩溃,也能说明问题。
上面我举的俩个例子,它们都是直接通过地址数据对变量进行访问的,我们已经知道直接通过对地址数据的访问其实是有很大弊端的,也会造成程序崩溃。
所以,C语言通过 int*p = &a这种指针变量的方式访问目标数据有什么好处呢?
它能够动态保存a的地址,不关心a的内容,达到稳定访问目标地址的作用。
好了,本文的内容就到这里了,本文部分注重理论部分主要的目的是让大家对于指针有一个大概的了解,这样在接下来的学习中才能更好的跟进。如果文章有任何疑问或者错误欢迎大家来评论区随时交流🙈🙈