【C语言入门篇】深入理解指针(1)
🌈个人主页:开敲
🔥所属专栏:C语言
🌼文章目录🌼
1.什么是指针?
指针就是存储地址的变量。
要想理解上面这句话,我们还得先从内存和地址讲起。
计算机内的数据,都是存储在内存中的,计算机中的CPU在处理数据的时候,需要的数据是在内存中读取的,那么这些内存空间是如何管理的呢?实际上就是把内存划分为一个个的内存单元,每个内存单元的大小是一字节。计算机中常见的内存单位有:
而他们之间的换算关系如下:
那么,这一个个的内存单元,我们是如何去读取的呢,这就要说到地址了。
在我们生活中,如果你要去你的朋友家玩,倘若不知道你朋友家的地址,那么你该如何去找呢,只能挨家挨户地去找或者干脆不去了,但是如果你的朋友告诉了你详细地地址(xxx小区xxx楼xxx号),那么你就可以根据这个地址直接找到他家。内存也是如此,内存中的每一个内存单元都有一个对应的编号,这个编号也就是地址,在读取这个数据的时候,只需要知道它的地址即可。那么这个地址应该用什么来存放呢?没错,就是指针,指针就是用来存放地址的。
2.理解和创建指针
既然指针使用来存放数组的,那么指针该如何使用呢?
指针变量的操作符为 * 。下面看一段代码:
这里我们创建了一个变量a,a里面存的值是10,然后我们将a的地址取了出来,那么如何用指针存放这个地址呢?
这里我们就是创建了一个指针变量 pa ,pa中存放的就是a的地址。那么前面为什么是int* 呢?
这是因为,我们a变量的类型就是int,因此,我们在创建用于接收a变量地址的指针的时候,需要创建与a变量相同类型的指针,后面再加上指针变量操作符 * ,加上了 * 就知道这个变量的类型是指针,就是int*了。那么同理可得,如果a变量的类型是char类型,我们就应该创建char*类型的指针,这里就不再一一列举了。
这里 pa 将a的地址存起来后,我们就可以通过pa去访问a中存储的内容,就好比你朋友将他家的地址写在了纸上,你就可以通过这个地址去找到你朋友的家,这里的 pa 就是这张纸,a的地址写在了pa上。
那么我们该如何通过pa去拿到a变量中的地址呢?这里就又要用到 * (解引用操作符),没错 * 还可以作为解引用操作符来使用。
用法如上,在pa前加上 * 就可以通过pa中存放的地址去找到地址所指向对象中的内容,这里打印的结果就是10。当然,我们不仅可以通过pa去访问变量a中的内容,我们还可以更改a中的内容。
这里打印的结果就是0,因为我们通过pa中存放的地址找找到了a,然后将a中的内容改为了0。
2.1 指针的类型
指针变量也是有大小的。不同的环境下,指针的大小不同,在32位机器上(x86环境),指针的大小是4个字节,而在64位机器上(x64环境),指针的大小是8个字节。
指针的类型也是多种多样的,存放int类型变量的地址,就用int*类型的指针,char类型就用char*类型的指针,那么不同类型的指针有什么意义吗?来看下面两段代码:
通过对这两段代码进行调试我们可以发现,第一段代码将a的4个字节全部改为了0,a就变成了0;而第二段代码只将a的1个字节改为了0,a的值变成了287453952,这就和指针的类型有关了。
指针的类型决定了指针在解引用的时候访问多大的内存。比如:char*类型的指针在解引用时,只访问1个字节;而int*类型的指针在解引用时,访问了4个字节。
这里要特别说明一个特殊类型的指针——void*类型。void*类型的指针不指向任何一种类型,也可以说,void*类型的指针可以指向任何一种类型。当我们不知道所接收的变量的类型的时候,就可以使用void*类型的指针。但是,需要注意,void*类型的指针不可以直接进行解引用操作,也就是不可以直接通过void*类型的指针访问地址。
2.2 const修饰指针变量
来看下面段代码分析:
函数text1是 *左边有const修饰的情况;text2是 * 右边有const修饰的情况;text3是 * 两边都有const修饰的情况,下面我们来逐个分析,首先看text1。
当我们运行这段代码的时候发现,系统报错了,我们再来看下面的报错提示——左值指定const对象,这个意思是,我们所指定的对象pa被const修饰了。那么这里的const修饰之后,为什么我们就不能对pa所指向对象的值进行修改了呢?这是因为,const在 * 左边修饰指针变量的时候,就限制了我们通过pa去修改指向对象中的内容。
下面再来看text2:
同样的,我们在运行的时候也发生了错误,这里提示写入访问权限冲突,这也是因为const修饰的缘故。
这里的const放在了 * 的右边修饰指针变量,限制了我们去改变pa所指向的对象。
最后是text3:
有了text1和text2,text3就非常好理解了,*的左右都有const修饰,那不就是将pa给限制地死死的了吗,你既不能通过pa改变a的内容,也不能将pa中存储的a的地址改为b的地址。
3.指针的运算
3.1 指针+-整数
我们知道,数组在内存中是连续存放的,那么这里我们就用数组来了解指针+-整数。
这里我们创建了数组arr,并将arr数组的首元素地址传给了指针变量parr,然后打印*(parr+1)的结果。这里打印的结果是2,为什么呢?
这就又可以回到上面指针的类型中的内容了。这里指针的类型是int*,+1之后就会跳过4个字节,而数组中的元素的类型也是int,int类型的大小也是4个字节,因此,parr指向数组中的第一个元素,+1之后跳过了4个字节指向了数组的第二个元素。那如果我们将这里的指针类型改为char*会怎样呢?
打印的结果为0,画图来分析
这里parr的类型是int*,当我们去访问parr+1时,跳过了4个字节,来到了arr[1]的地址,因此此时的parr存放的就是arr[1]的地址。而当parr的类型是char*时,
parr+1只跳过了1个字节,指向了00的位置,因此,打印的结果为0。
3.2 指针-指针
还是先来看一段代码:
想一下这段代码的打印结果是多少。
这段代码的打印结果是3,为什么呢?我们来逐个分析。这里定义的函数my_strlen中,形参部分创建了一个char* s 的指针变量,因此可以知道,这里的形参存储的是主函数中实参的地址(这里是字符串的首元素地址)。函数内部创建了char* p 的指针变量,用于接收s的地址,也就是主函数中字符串首元素的地址。后面while循环中的条件是*p!='\0',循环内部p++,因此可以判断出,这个循环用于定位字符串最后一个元素的地址。最后的return p-s是两个指针的相减,指针相减实际上就是地址相减,那么最后的输出结果为什么是3呢?我们可以在函数中打印一下p和s的地址来看一下
可以看到,p和s的地址恰好相差3(16进制中的B是11)。前面我们知道,s存放的是字符串首元素的地址,因此就是字符串中a的地址,而p是字符串最后一个元素的地址,也就是'c'的地址,而字符的大小是1个字节,因此,p和s之间相差3个字节的大小,因此,最后打印的结果就是3。
4.什么是野指针及如何避免野指针的出现
4.1 什么是野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的)
野指针的成因:
1.创建指针变量时没有初始化:
2.指针越界访问:
这里我们创建了数组arr,arr中只有10个元素,指针变量p存放数组的首元素地址,后面的循环,通过指针将数组的元素进行赋值。问题就出在循环中,循环的次数是0~10也就是11次,但是数组只有10个元素,因此,当i = 10也就是第11次循环的时候,指针指向的空间不再属于数组,使得指针指向了一片未开辟的空间,导致指针越界访问。
3.指针指向的空间被销毁(释放)
text函数的空间在text被调用完以后会还给计算机,也就是被释放了,此时p如果指向text,就会导致指针成为野指针。
4.2 如何避免野指针的出现
1.习惯创建指针时初始化
2.小心指针越界访问
3.当指针不再使用时,将指针置为NULL: 当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
4.避免接收局部变量的地址
5.指针的使用——传值调用和传址调用
5.1 strlen函数的模拟实现
库函数strlen是用于计算字符串的长度,统计的是字符串中'\0'之前的字符个数。
函数原型:
const char* str用于接收字符串首元素地址,函数返回类型为size_t(无符号整数)。
下面我们来使用指针模拟实现一下strlen。
这里的my_strlen就是我们模拟实现的strlen函数。const char* str接收字符串s的首元素地址,while循环用于遍历字符串,直到遇到'\0'停止,a用于记录循环的次数(也就是‘\0‘之前的字符个数)。
深入理解指针(1)完.