欢迎来到Cefler的博客😁
🕌博客主页:那个传说中的man的主页
🏠个人专栏:题目解析
🌎推荐文章:题目大解析(更新ing)
目录
指针是什么?
在学习C语言的过程中,指针经常被人觉得是一个比较难理解的一个知识点。但其实最主要原因,是因为我们没有搞懂指针它到底具体指的是什么,会将指针和指针变量搞混淆,而我们为什么会经常将二者给混淆的呢,首先我们需要先了解一下指针的两个要点:
指针
是内存单元中最小的单元编号,是地址,所以,指针的本质就是地址,地址就是指针- 我们平常口头上的指针,一般都是指针变量,与指针不同,指针变量是用来存放指针的,也就是地址
那么问题又来了,什么是地址呢?指针变量又是怎么存放地址的呢?
地址
我们知道,电脑在接受外界信息的时候,一开始接受的是电信号,后面经过处理加工后才变成数字信号,数字信号也就是供电脑分析的二进制,这个转换过程是比较复杂的。这里我们来分析电信号如何在地址上变为数字信号,就以32位机器为例子,这里就可以认为有32条地址线,在地址线开始寻址的时候,地址线上就会发生生高电平(高电压)和低电平(低电压)就是(1或者0),而每一条地址线都有2种可能,要么是1要么是0,所以32条地址线产生的地址就可以达到2^32种
那么这是多大的内存空间呢?一般规定,内存中一个单元是1个字节,而一个地址对应一个单元,也就是说32位电脑因为2 ^32种地址,有所拥有的内存空间大小为2 ^32byte,换算成GB就是4GB。我们为什么要引出地址这个概念呢,其实说白了就是,就比如说我们的内存空间一共有4GB,每一个小单元是1个字节,在我们向内存中不管是输入还是输出信息,我们都得找到相应的内存单元,而寻找就需要地址去寻找,可以说一个地址就是一个单元的门牌号,我们根据这个门牌号才能找到所需单元的位置,而这才是地址这个概念出现的初衷。
指针变量存放指针(地址)
我们上面讲过了地址,可以知道,地址的表现形式是二进制序列,32位机器中有32个0或1组成二进制序列,所以在32位机器中,一条地址若想存放,就需要32byte位,也就是4字节,所以,一个指针变量的大小就是就是4字节
指针和指针类型
指针也是有类型的,但指针需要类型吗?我们首先会这样问,比如像int类型在内存中占4个字节,char类型需要1个字节,而指针类型所需要的空间大小是多少呢
我们会看到,不管是整型指针还是char类型指针或是其它类型指针的存储空间大小都是4个字节,这个其实也不奇怪,我们在上文中已经说了,指针变量是用来存储地址的,所需要的空间大小就是4个字节。
但我们要问的是,既然,不管是什么类型的指针存储空间都是4个字节,那为什么不干脆只用一种类型存储地址,搞那么多干嘛?
而其实,指针类型的意义有两点:
一. 指针类型的作用主要是决定了解引用操作符访问的时候能访问几个字节
在这里,我们首先令a等于一个16进制数(为了待会调式时方便看),而后创建整型指针变量存放a的地址,而后用用解引用操作符寻到a的地址,然后并赋值为0。这一系列操作在内存调试中和我们预料的是一样的。
而这个时候,如果将整型指针变量改为char类型指针变量结果是否会发生不同呢?
我们会惊奇的发现,类型改为char后,*p=0只改变了1个字节位的值(0x11223344是16进制,一个16进制位占4个二进制位,占4byte,而2个16进制位就是占8二进制位,也就是8byte,1字节),这是为什么呢,我们这个时候再来理解“指针类型的作用主要是决定了解引用操作符访问的时候能访问几个字节”这句话,在之前int类型的时候,改变了4个字节,而char类型的时候,只改变了1个字节,相必这时大家应该能感觉到这之间的一些关系了吧。
而当类型变为指针变量的时候,这些所占内存空间大小的意义变为解引用操作符访问的时候访问的空间大小.
二、指针类型决定了,指针进行±1的时候,一步走多远。
如图上,我们对int类型和char类型指针进行了+1,可以发现,进行+1后的指针所指向的地址跨度空间,int型的指针是4个字节, 二char类型的却只有1个字节,而这个说明了,指针类型决定了,指针进行±1的时候,一步走多远。
我们可以这样理解这句话,就像1+1=2,a+a=2a,这些进行运算的都是同一种类型,而当我为int类型指针的时候,我进行+1,因为我是Int型的,所以这里的1可以理解为1个Int,一个int所占内存空间是4个字节,所以+完之后,走了四步(跨越4个地址)。同理,char类型指针+1,加了1个char,1字节,跨越一个地址。这样,你能理解了吗。
这里我们应该能搞明白指针类型的作用大概是什么了,但是这有什么用呢,我用一道题来帮大家理解下
将数组arr[10]={0}中的元素初始化为1 2 3 4……10
int main()
{
int arr[10] = { 0 };
int i = 0;
int* p = &arr[0];
for (i = 1; i <= 10; i++)
{
*p = i;
printf("%d ", *p);
p++;
}
return 0;
}
这里,我们创建了一个int类型的指针变量接收了数组arr的首个元素的地址,而后在循环中,用解引用符寻到元素地址,给其赋值,每进行一次循环,进行p++,比如第一次循环里,p指向的是arr[0]元素,p++后,此时所指向的地址变为arr[1]的地址,这样,就可以给数组中所有地址挨个赋值初始化了。
但是一些细节我们要注意,因为这里的前提是我们创建的是整型数组,整型数组中的每个元素都是占4个字节空间大小,而又因为指针类型为也为整型,所以p++后跨越的地址是4个地址
而如果是char类型指针呢,会发生什么呢?
这个时候大家就会觉得,这个不是没有变化吗?但其实,变化了。
我们在内存调试中可以看到,每次p++后的跨度只有1个字节,也就是说,每次循环只跨越了一个字节,而数组里面每个元素的地址跨度是4个字节,这样每次循环赋值时,根本匹配上要被赋值的数组元素,理应来说此时打印出来的结果肯定不是1 2 3 4……10。
那么为什么打印出来的数值没有变化呢,其实问题出在打印的对象
我们这边打印的对象是*p,我赋值赋给的是 *p,所以不管是什么类型,不管是+1还是+2,我打印出来的结果肯定是我所赋的值。所以我们这里直接打印arr数组看。
如图,可以看到其它位置都还是0,这就说明刚刚赋值的时候根本没有赋到相对应的数组元素,地址不匹配。
野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
我们一般在创建指针变量的时候,没有给指针进行初始化,这个时候指针就变成了野指针
顾名思义,野指针,它并没有确定指向哪个地址,它的位置是不确定的,你可以将它理解为无家可归的流浪狗,在茫茫天地处寻不到一处安身之地,而此时它流浪在天涯,随遇而安,所行便是安身处,任何地方都属于它,但是,它不属于任何地方。就比如下方一个例子
我们这里创建了一个野指针,因为野指针的方向是不确定的,我们这里假设野指针随便指向了一个地址,为地址a,然后这里我们想给这个指针赋值为10,所以我们顺着这个地址a找到相应的内存单元,但事实是,我们根本无法给这个内存单元赋值为10,因为这个内存单元根本不接受这个指针,因为这个地址本身可以说是“违法”的,它来路不明.
我们可以这样理解,我们平时创建一个指针变量,然后给它初始化,此时指针已经指向了一处地址,也就是向该处地址的内存单元打了个招呼,已经熟悉过了,后面再用*解引用操作符顺着地址去访问内存单元的时候,因为是熟人,所以可以顺利的赋值。
但是野指针根本没有进行初始化,也没跟别人打过招呼,你冒然找上人家,并要为其赋值,人家跟你又不熟,怎么会赋值成功呢?就像是陌生人敲你家门,你会随便给他开门吗?显然是不会的。
所以,为了让指针不要变得无家可归,我们在创建指针变量的时候,要记得给它初始化。
但是,如果实在不知道要初始化什么的时候,我们可以给定义它为空指针
指针运算
指针±整数
在上文的指针类型中,其实我们已经接触到了指针±整数这一概念,比如像设置
int arr[]={1,2,3,4,5};
int* p=arr;
printf(“%d\n”,*(p+1))
在这当中,p+1就是指针+整数,那么它得到的结果是什么呢?我们在判断结果是什么的时候,首先要先关注这两点
- 该数组是什么类型的(这决定了数组中单个元素所占内存空间大小)
- 指针是什么类型的(这决定指针±整数中,这个整数所能跨越的长度是多少)
就拿上面的代码为例,我们可以分析得到
1.数组类型为整型,由此得到单个元素所占内存大小为4字节
2.指针类型为整型,由此得到指针±整数中的整数能跨越4个字节长度
指针-指针
我们知道,指针就是地址,指针减去指针,我们是不是就可以理解为地址减去地址?
这样理解似乎也没错,就比如高地址减去低地址就可以得出它们之间的距离大小。
那我们来试验一番
如上图,我们(p+10)处的地址也就是\0处,减去p处的地址,也就是数组首元素的地址,因为是整型数组,单个元素所占空间大小为4字节,所以数组首元素与\0的地址距离长度应该为40字节,所以按理来说这边的打印结果应该是40字节,那么为什么打印出10呢?
大家千万不要以为(p+10)-p就是p-p剩下10那么简单。
其实在C语言规定中,在数组当中,指针-指针其实得到的是在这两个地址中间有几个元素,因为我取的地址是\0的地址-首元素的地址,而这两个地址之间有10个元素,所以打印结果为10.
那么指针-指针有什么前提条件呢?
- 两个指针指向同一空间
如以上指针指向的空间都是arr这一数组所在的内存空间 - 指针类型必须相同
指针的关系运算
现在有一道题,要求是将数组中的元素全部初始化为0,但必须得用指针。我们这里给出两个版本;
版本一(推荐版)
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = NULL;
for (p = &arr[10]; p > &arr[0];)
{
*--p = 0;
}
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
版本二
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int* p = NULL;
for (p = &arr[9]; p >= &arr[0];p--)
{
*p = 0;
}
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
我们一对比这两个版本,其实差别也不大,甚至说只是进行了细微的改变,版本二只是在版本一的基础上将for循环中将p–这一动作放置在了赋值后面。但是我为什么要专门举这两个版本呢,我们首先得先了解一个规定
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
这是什么意思呢?
如图我们就可以来理解这个规定是什么意思,也就是说,末元素的指针可以被允许与指向区域二(也就是arr数组内存区后面的区域)的指针进行比较,而首元素则不被允许与区指向区域一(也就是arr数组内存区前面的区域)的指针进行比较。
可以看到我给出的这两个版本中,我是推荐版本一的,也就是说版本二其实就存在触犯规定的地方。
在版本二中,当p循环到指向首元素地址的时候,将0赋值给首元素之后,又要进行p–,而这个时候,p指向的地址就是arr数组内存区域后面的区域了,这个时候p又要再和首元素的地址进行比较,这就违法了我们的规定。
其实,在一些编译器中,代码运行起来时其实也不会出现问题,但是为了不出现问题,我们还是要尽量遵守这个规定,那不然哪天换一个编译器对这种违规介意,那么就会出问题了。
二级指针
二级指针定义:即用来存放一级指针的地址的地方
如图就是二级指针存放一级指针的使用
指针数组
指针数组,顾名思义,就是存放指针的数组,数组中的元素类型都是指针
我们通过将变量的地址存放在数组当中,然后再运用指针解引用也可以访问到我们的变量
指针数组打印二维数组
小知识点:
arr[i]、i[arr]、(arr+i)、(i+arr)四个表达是等价的
所以arr[i][j]等价于((arr+i))[j]等价于(*(arr+i)+j)
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长