指针 --1
内存和地址
学习指针之前我们先来了解一下内存和地址
比如说你朋友住在某个宿舍,你去找他,你来到宿舍楼,他没告诉你 他住在几楼,此时你最有效的方法就是每个宿舍每个宿舍的找,但在实际生活中,每个宿舍都有它各自的编号,那如果拿到这个编号那是不是就可以找到这个宿舍呢?同理我们可以试着把它放在计算机里看看。
1、内存
我们计算机是要处理大量的数据的,数据都是CPU上处理的,那数据从哪来呢?从内存里来。内存我们知道有的电脑是16G、32G,总之就是很大的一块空间,那我们说把他比喻成在一栋大的宿舍楼里找到你的宿舍靠的就是地址,那么CPU在这么大的内存空间里找到这个数据,靠的是不是这个数据的地址呢?就是这样的,在计算机里面,早就把
内存划分为一块一块的内存单元,每块内存单元的单位是一个字节
并且每个内存单元都有它各自的编号,也就是它的地址,就像在宿舍楼里,每个宿舍的编号。
C语言给地址取了一个新的名字:指针
其实指针就是地址,
内存单元编号==地址==指针
2、编址
计算机有这么多硬件,那它们是这么链接同步的呢?靠线,有数据总线,控制总线....,我们主要讲地址总线,CPU要计算数据,就要从内存拿数据,这叫读取,控制总线就变为Read,那这么读取数据呢?CPU有一个地址,通过地址总线,在内存里找到数据,回去CPU里计算,计算完后再把数据放回内存里去,这个叫写入,控制总线变为Write。
每个内存单元的编址,并不是给每一个内存单元写好编号,再存来用的,而是本身硬件电路就设计好的,比如吉他,并没有把每个音符都写上去了,但是懂音乐的就是会谈,同样的道理。本身就是设计好的,给我地址,就能自动的找到内存指向的那个位置。
在32位机器上有32根地址总线,每跟地址总线能表示0或1的二进制位,总共能表示32个二进制位,对应的就是我们地址,地址其实是十六进制显示,由8个十六进制位组成的,一个十六进制位表示4个二进制位,也就是32个二进制位,对应32跟地址总线,就会通过0跟1组成地址,通过硬件电路找到对应的内存空间。
指针变量和地址
1、& 取地址操作符
每个内存单元都有地址,可以打印出来,使用取地址操作符 &
在X86环境下,地址会更好观察
打印地址,使用%p打印
0x1122334是一个十六进制数字,32个二进制位,a是一个整形,4个字节,是可以存下的,对应的就是4个内存单元,也就有4个地址,可以看到a的地址存进去这个数值,但是a是4个字节,有4块内存单元,4个地址,那那一个地址是a的地址呢?
其实a的地址是4个字节当中第一个字节的地址,也是最小那个地址,
那a的地址有4个啊,为什么只打印一个呢?
虽然打印一个地址,但是它们是连续的地址,只要知道第一个地址,其他顺藤摸瓜就可以找到
2、指针变量和解引用操作符 *
我们取出来地址,可以看到地址其实十六进制的数字,那它也是一个数值,能不能存起来呢?
可以。
把&a的地址取出来放到pa变量里去,pa的类型就是int*。pa也叫做指针变量。
指针变量不难理解,把a的地址放到变量里,那就是存放地址的变量,地址就是指针啊,那就是存放指针的变量,也就是指针变量--存放地址的变量。
那指针变量的类型怎么理解呢?int*
可以把它拆分成两部分;* 代表这是一个指针变量,int表示这个指针指向的对象是int类型的;
如果把b的地址存到pb里面去,那就是char*pb=&b;
我们把地址存起来了,就像相当与我们拿到了这个房间,那这个房间我们可以把东西放进去,也可以拿出来啊
要想读取这个地址里面的数据,就要使用* 解引用操作符
比如把n的地址存到pn里面去,pn是一个指针变量里面存的是地址,我们再对pn进行解引用操作就可以找到n的这块空间里面的数据了。
相反,我们拿到这块空间的数据,拿也可以往这里面放数据啊
同样是对pn进行解引用操作,找到n的内存空间,往里面放了个20,结果n的值就被改了。
3、指针变量的大小
我们知道指针变量的创建了,拿指针的变量是多少呢?是4个字节,还是8个字节呢?
指针变量顾名思义就是存放地址的变量,那地址多大,变量不久有多大吗?
在x86环境下,32位机器,有32根地址总线,地址是32位个二进制位,一个地址就是4个字节(1个字节=8个比特位),那指针变量就是4个字节
在x64环境下,64位机器,有64根地址总线,地址是64位个二进制位,一个地址就是8个字节(1个字节=8个比特位),那指针变量就是8个字节
可以看出来指针变量的大小与你的指针类型,只要在相同的平台下,指针变量的大小都是相同的。
无关指针类型,因为,不管你是什么类型,都是地址嘛,你是32位机器,那你的地址就是32个比特位,4个字节;你是64位机器,那你的地址就是64个比特位,8个字节
指针变量类型的意义
那指针变量的大小与指针的类型无关,那为什么还要指针类型?不是大小都一样吗?
1、指针的解引用
例子,把整型a的地址放到pa里,地址都是一样的,是可以放的,可以看到内存,这个值是确实放进去了,
可以我想把a的值变为0的时候,怎么才把第一个字节的数据变成0了?
这就是指针类型的差异了,pa的指针类型是char*把,编译器就说,你指向的对象是char类型的啊,那我通过这个地址访问你的时候 ,那我就一个字节一个字节的访问啊。这就把第一个字节变成0了。整型有4个字节,但是char类型只是访问一个字节;
那我们可以得出结论,如果你想几个字节的数据,那我们就要考虑清楚,要选那个指针类型。
如果你想把a变为0,那我们就是访问它整个4个字节,那pa的指针类型就是int*,说明,你指向的对象是int类型,那访问你4个字节的数据。
2、指针+-整数
把a的地址给到pa、pb地址是一样的,如果给pa、pb各加上一个1呢?
可以看出来给指针pa+1,28变成2C,多了4个字节;
给指针pb+1,28变成29,多了一个字节;
这样的变化其实是和指针类型是有关的;
pa指向的是int类型,访问4个字节,那pa+1,就是访问四个字节之后,+1,那就是跳到下图的位置了,跳过了4个字节
pb指向的是char类型,访问1个字节,那pb+1,就是访问1个字节之后,+1,那就是跳到下图的位置了,跳过了1个字节
结论,我们可以根据指针类型,可以让指针往前走或者往后走多少步,可以+1,-1。
比如我们访问一个整型数组,我想一个一个字节的访问那我们就用int*,如果想一个一个字节的访问我们就用char*。
不但也可以+1,也可以+2,+3,+n
1*sizeof(int *) 访问4个字节
1*sizeof(char *) 访问1个字节
n*sizeof(x *) 访问n个字节
3、void*指针
指针类型还有一种 void*,无具体类型的指针(泛型指针),通常是用来接受任意指针类型的,
比如随便给我一个地址,但是我不知道这个地址的类型,那我就用void*存起来,等我用了,我在拿出来用;
void*它也有不好的地方,就是我既然不知道你具体是什么类型,那我不知道要访问你几个字节啊,你指针+-整数,我也不知道要访问几个字节啊,所有void*的指针是不能解引用和指针+-整数的,
如果非要访问它,那就要把它强制类型转换,再去解引用。
const修饰指针
const的意思是常属性
const可以修饰变量或者指针变量
const修饰变量和const修饰指针变量
int a = 10,我可以修改这个变量里面的值,但是如果我在前面加上一个const修饰它,那这个变量就不能被修改,a本身是一个变量,但是被const修饰后变成一个常变量,但是本质上它还是一个变量,编译器在语法层面上不能对它进行修改。
C99之前,变长数组是不能使用,是因为变长数组给你元素个数是一个变量,如果在变量前加上const变成一个常量呢?还是不行,原因是它本质上还是一个变量,但是在C++哪,const修饰之后的变量就是常量;
既然被const修饰的变量不能被修改,如果我拿出它的地址,访问它是不是可以修改它呢?
你会发现没有任何的问题;但是这样破坏了规则,你本来使用const就是不想有人修改这个变量啊,你通过这样的方式修改我的值,怎么行啊,你修改,他也修改这不就乱套了?
既然const可以修饰变量,那const是不是可以修饰指针变量呢?
指针变量无非就是两种用途:1、指针变量本身可以放置其他变量的地址 2、解引用同过地址访问指针变量指向的对象
其实const修饰指针变量有三种方式;
1、const放在*号的左边
表示指针变量指向的对象是不可以修改的,但是指针变量本身是可以修改的。
2、const放在*号的右边
表示指针变量指向的对象是可以修改的,但是指针变量本身是不可以修改的。
3、const放在*号两边
表示指针变量指向的内容不可以修改,指针变量本身也不可以修改
指针运算
指针+-整数
前面我们讲过,我们把它运用起来。
我们访问这个数组内容,我们知道指针+-整数,假设我们拿到数组第一个元素的地址,数组名也就是数组的起始地址;把它放到p,对p解引用那我们是不是拿到第一个元素,如果访问第二个元素,那地址是不是地址p+1,p+1跳过一个字节+1,就是跳过第一个元素,+1,就是第二个元素了,对p+1解引用就取到第二个元素了,那第三个元素,就是跳过两个字节,p+2,以此类推...
那p+0,p+1,p+2,p+3...我们可以抽象的认为是不是p+i,取出第一个元素的地址,再遍历每个元素的地址,对地址进行解引用,就可以拿到数组的每个元素了。
拿到数组的每个内容其实有多种写法;
这样也是可以的,p++,每循环一次p+1;但是这种方法有一点不好的是p是再变的,而上面的方式只是i在变,p没变;
倒着打也是可以的;
指针-指针
指针-指针 得到的是元素个数的绝对值;
把下标为9的地址-下标为0的地址,得到的是地址之间的元素个数;如果调换过来,下标为0的地址-下标为9的地址那得到的就是-9;
指针-指针 有一个前提就是两个指针指向必须是同一块空间;
下面的代码是错的,它们并不是同一块内存空间,而且计算的结果是按照int类型算,还是按照char类型算呢,也不确定,这是错误的。
假设我们求arr数组里面的字符个数,可以用到strlen来计算它的字符个数;
arr数组除了存放a b c d e f 还有一个‘\0’,放在后面;
strlen计算的就是\0后面的字符个数;
那我们可不可以自己来实现一个strlen?我们可以创建一个my_strlen。
strlen的返回值类型是size_t。
我们把arr给到my_strlen,arr是数组名是数组的起始地址,那我们就用char* 的指针接收,计算字符个数,到\0就结束,那我们把它拿出来看看,是不是\0,如果不是\0,那我们就计数count++,同时我们向前走一位,继续看是不是\0,不是的话再++,这样就是一个循环,如何不是\0,就一直循环下去,直到\0就停止,最后返回的是count,计数的结果。
当然我们还有一种方法;
指针--指针的得到的是元素个数,那把arr数组的最后一个元素的地址-起始地址,那不就是arr数组的元素个数了吗?确实是,先把起始地址存起来start,再把s遍历到最后一个元素的地址,它们相减就是元素个数。
指针的关系运算
指针的运算关系就是比较两个地址的大小,一个地址是有大有小的,比如数组每个元素的地址有低到高增长的,那么低的地址就是小,高的地址就是大。
假设有10个元素,对应的0~9的下标,如果我们取第10个元素的下标,那我们肯定就会比起始地址大,比起始地址大就是为真,就循环打印每个元素,p++,,打印下标0~9的元素,遇到下标为10的地址,两个地址相等,那条件为假,结束循环。
野指针
野指针就是它的地址是不知道的;随机的,这个地址不是我自己的。
指针未初始化
创建了指针变量p,对*p解引用给它个10;这个p就是野指针;
为什么?因为p没有明确的给它一个地址,编译器就会随便给一个地址,就像变量没有给定一个值,编译器就是给它一个随机值,同理编译器给我一块随机的空间,这块空间不是我自己的啊,我没有访问权限,那就构成非法访问了,p就变成野指针了。
指针越界访问
比如下面这个代码,把数组的10个元素打印出来,本来是循环10次,现在循环了11次,那是不是访问了这个数组后面的空间,是不是构成非法访问了,这块空间不属于你啊,你拿到这块空间地址的时候就是野指针了,非法访问。
指针指向的空间释放
例如下面的代码,把test函数的返回值放到指针变量p里面,再对p解引用打印,test函数里面有一个整型变量n=10,返回值的n的地址;这里有错误;
是什么错误呢?
还是野指针;为什么?
函数栈帧的销毁,程序进到test函数,整型变量创建了,返回去n的地址,test函数调用完之后,那变量n的空间就换给操作系统了,那当p拿到n的地址的时候,p就是野指针了,n的空间都会给操作系统了,你再拿n的地址还有用吗?n的空间已经不属于你了,你再去使用,不就构成非法访问了吗?
如何规避野指针
那我们如何规避野指针呢?如果明确知道指针指向那里,那就直接给它赋值,如果不知道指针指向那里就给它赋值为NULL;
NULL就是空指针的意思,NULL就是0,0也是地址,但是如果对NULL解引用就会报错;
指针初始化
如果不知道指针变量p指向那里,就给赋值NULL;如果再对p解引用是会报错的;
小心指针越界
就像上面的数组,一定要在数组本身的范围内,如果超过了数组本身范围,去访问别的空间,就是野指针,指针越界了。
指针变量使用之前检查其有效性
我们可以把野指针想象成野狗,野狗是没有主人的,很危险,如果我们把它栓在一个树上就会安全多了,给野指针赋值NULL,就是把野指针栓在树上,把野指针管理起来;
如果指针使用完后,不想再继续使用它了,我们可以给它赋值NULL,有一个约定俗成的规定,就是指针变量赋值NULL就不再去碰它;
如果后面再继续使用,可以给它赋值一个新的地址;使用之前也可以给指针来个判断;
if(p!= NULL),如果p不等于NULL,那就可以继续使用它。
避免返回局部变量的地址
上面的test函数返回的就是局部变量n的地址,当它的函数栈帧销毁的时候,n的空间就换给操作系统了,拿到n的地址那一刻,就构成野指针了,所以要避免返回局部变量的地址。
assert断言
asser就是根据assert里面的表达式,如果为假,那么程序就会停止运行,报错;如果为真,那么什么事情都不会发生。
比如下面代码,p是一个空指针,assert判断p不是空指针,程序运行编译器就会报错。如果p不是一个空指针,那什么事情也不会发生,程序正常运行。
使用asser要包含头文件#include<assert.h>
使用assert断言的好处就是,如果不满足条件就立即报错,直到满足条件才会程序才会运行下去,如果你不想使用它可以再头文件前面加上 #define NDEBUG ,那assert就不会生效了,想继续使用的话,就把define那一条屏蔽掉就好了,assert就可以正常使用了。
不好的地方就是使用assert会增加程序运行的时间,但是release版本会自动把assert给屏蔽掉;
指针的使用和传址调用
对于我们之前写的my_strlen,我们还可以对它优化一下;
我们传给my_strlen是让它计算字符个数,并不像修改这个数组本身的内容啊,如果my_strlen,写错了修改了数组里面的值,那就不好了;
那我们可以再给my_strlen的参数里,加上const修饰,那在my_strlen里面就不会通过指针变量来访问对象,修改里面的值了。
还有*s!=‘\0’, ‘\0’就是0,*s是a,a的ascii码是97,不为0,循环条件就为真,如果循环条件为假,那循环结束了,结果也是一样的;
传值调用和传址调用
假设给个函数,将两个变量的值进行交换;
可是发现它们之间没有交换啊
当我们打开监视窗口,看到a,b的地址和x,y的地址是不一样的,x,y形参它们是有独立的内存空间的,所以你同过z,将x,y这两个值调换,会影响到a,b吗?不会的,这个叫叫做传值调用,就是我把值传给你,就是计算一些数值,然后返回给我,但是不会改变形参;
那如何实现把a,b两个数的值进行调换呢?
既然x,y指向的是不同的内存空间,如果把a的内存地址给x,b的内存地址给y,那它们是不是就可以通过地址,来找到它们的内存空间呢?
把a,b的地址传过去之后,就会通过地址找到a,b的内存空间,将它们的值进行调换,这个叫做传址调用,就是把地址传过去;
传值调用和传址调用的区别就是,一个传的是值,一个传的是地址,传值,是不会影响到实参的,大部分时候,如果就是想把实参的数值进行计算,最后返回一个结果的话,使用的就是传值调用,如果一个函数调换两个数值,影响到实参的话,就只能用到传址调用。
感谢