声明:我是跟着B站江科大的视频的学习过程中记录下来作者的文案,记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以一起学习。
我把其中一些白话进行了修改,并且添加了自己的一些理解。我只有一些pyhon基础,所以可能有错误,学起来也比较吃力,就把自己的一些理解加上去了,方便大家有和我一样没有基础的人进行学习,如果有不对的地方欢迎指正
在讲之前,先要知道像int、char、float这些是叫做变量类型。
int a;
char b;
这里的a和b是叫做变量。
指针讲解分为两个部分:详解、应用。
一、详解
指针Pointer,使用灵活、功能强大,是C语言的灵魂。
指针与底层硬件联系紧密,使用指针可操作数据的地址,实现数据的间接访问。我们讨论指针就会讨论内存这些东西,因为指针就是一种地址,所以它必须要和内存实际存储的数据联系起来。
存储器有两个重要的点,一个是数据,另一个就是地址。所以如果把地址也操作起来的话,那我们就能更加灵活的去使用这些数据。
指针如何间接访问数据呢?我们平常定义一个变量,用变量的名字可以直接引用这个变量。
但是我们还有另外一种方式,就是间接访问。我们可以把这个变量的地址拿出来,通过指针去寻找这个地址下的变量,实现一个间接寻找的过程。
那我们为什么不直接使用变量名直接访问变量呢?这个间接访问可以起到很大的作用,具体的在后面结合例子进行分析。
上面讲了指针和内存是离不开的,所以在分析指针之前,先讲一下计算机是如何存储数据,内存是如何定义的。
我们通常会把内存分配成这样的一个线性区域,每个区域都是以字节为单位(内存以8位为一个单位,也就是一个字节),每个字节都会对应一个地址,如果字节不对应地址,那就访问不到了。所以为了访问这个数据,每个字节都会有它独一无二的地址。
看上图,在计算机系统里,int代表了一个4字节的数据,也就是一个int类型的数据,要用32个二进制来表示。看右边,由于a要跨越4个字节的长度,所以一个int型变量就跨越了4个地址,把小端放在内存地址的低位,就是放在前面,这种分配方式叫做小端模式。还有一种把大端放在前面的叫做大端模式。但是现在计算机一般是小端分配的。
数组是按顺序来存的,数组在内存中必须分配一个连续的线性空间。中间不会隔几个其他的数据再继续存,必须是连续的。数组里每个数据的内部是按小端存储的。
指针即指针变量,也是一种变量,和一些int、short型变量的基本形式是一样的,用于存放其他数据单元(变量/数组/结构体/函数等)的首地址(弹幕说指针是一个地址,指针变量是存放地址的变量,所以指针即指针变量好像是有点问题)。若指针存放了某个数据单元的首地址,则这个指针指向了这个数据单元,则这个指针为空指针。(指针是一种变量,存在内存里,保存的是所指向数据的首地址)
因为一个变量/数组等可能跨越多个地址,指针变量没法存,所以只能存其他数据单元的首地址。
我们定义一个变量int a;,这个int就是变量的类型,同样指针也是一个变量,那如何来定义呢?
根据指针指向的一个变量的不同,对应的都有一个指向该数据类型的指针,
“*”只是个变量类型的数据类型的一个标识符,表示它是一种指针的类型。
指针变量的位宽是和系统一样的。这是因为指针是用来存储某个数据单元的首地址,那指针变量跟我们说只要够存,存的下,我们就定义这些。对于64位系统来说,它里面的内存可以达到2的64次方,这么多地址。那如果我们定义一个变量去存这么多地址,至少要保证这个变量能够把所有的地址都存的下。所以我定义一个64位的指针,就能够把任意一个地址给存下来。所以无论指针指向什么数据,它在指定系统内的位宽都是指定不变的。
先定义一个int型变量int a;,在定义一个int型的指针int *p;,这个int和*是一个整体int *,这个整体表示后面的这个p是指向int型的指针变量。
int* p, p1;表示p是指针的类型,而p1就是int类型。
查看int型和int指针型变量所占的内存,可以看到分别是4字节和8字节。
“&”后面跟一个变量,前面没有的话就是取地址。
“*”后面跟一个变量,前面没有如int的变量类型的话,就是取内容。
&p是指取地址的地址。
*p是取p指向的 地址 里的 数据
指针p也是一个变量,也会占用内存(只要是变量系统都会分配地址),有8个字节,并且指针也会有首地址,如果取这个首地址就是二级指针。
执行完p=&a;这句代码时,p就是对a取地址。因为a的地址位宽也是64位的,8个字节,p就会把这个地址取出来存在p这个变量里面。相当于给p赋值,这个值就是a的首地址。
由上图可以看到a的值是66,这是没问题的。P的值是62FE47,说明a是存在地址62FE47下的。
p=&a; 说明p指向了a。
*p就是一种间接访问,程序会首先看一下p存的是什么,而不把p打印出来,而是把这个p的内容作为一个地址,去找一下这个地址上存的是什么内容。那以什么形式来打印呢?就取决于这个指针的数据类型,如果这个指针是char型,那他就会找到这个地址,并且把这个地址和它后面的变量,以一个char型的方式翻译出来。这就是指针的执行过程,间接的,就是先找地址,进行一个跳跃,再打印出内容。也就是上面说的,使用指针可以操作数据的地址,实现数据的间接访问。
如果单纯的定义一个变量,那就直接使用变量名访问即可,不需要间接访问。
指针作为一种变量,那就说明他就可以运算,只不过指针变量运算一般没有什么乘除、开根号这些复杂运算,因为它只表示地址。他一般只包含两种运算,一种是地址加,一种是地址减。
原来的p=62FE44,++后的p=62FE48。它加的并不是我们想的+1等于62FE45,这是因为它加的并不是一个值,而是一个数据宽度。而且int类型的数据占4个字节,所以是+4。如果是char的话就+1,因为char型数据占1个字节。它的加减都是以它指向的类型作为一个基本单位。
如果指针指向了一个单独的数据,那指针加加减减就没有意义了。因为单独的数据就占了一个数据宽度,当++时,指针指向了下一个地址,而下一个地址的内容我们事先没有定义,所以里面到底是啥我们不知道,指针此时就越界了,指针访问到了非法位置,可能会导致程序出现错误。
指针的加减一般出现在数组里。我们让指针指向数组的首地址,而数组后面的地址是我们事先定义好的,是已知的,这样地址加减才有意义。
下面来看一下与指针有密切关系的数组。
数组就是一些相同数据类型的变量组成的集合,其数组名即为指向该数据类型的指针,是指向数组首地址的指针,这表明数组也是指针的一种表述形式。
利用下标引用数组数据也等效于指针取内容。下面来验证一下。
数组名也是地址,所以p=a是可以的,a既是数组名又是指针,所以a前面不需要加“&”来取地址。
由结果可知,数组取下标其实就是指针取内容的另一种表示形态。
直接对a操作也是可以的,因为a其实也是一个指针。
见下图,如果是int类型的数组的话,为什么要加4呢?因为int变量类型的变量占4个字节的内存,所以他需要用四个地址来存一个int的变量。a就指向了这个数组的首地址,a+1就是偏移了一个数据宽度,即4个字节的内存,相当于一下子跳了四格,就可以取到第二个数据。地址+1是根据类型宽度来决定的,这样才能和数据对齐。
同样,这个数组的定义我们也可以用指针来翻译它,即下面的过程(只不过比较麻烦)。
需要先申请一个内存:
malloc(3*4); 首先是3个变量,int类型的每个变量占4个字节,所以是3*4.其返回值是一个void *,他表示一个指针指向void型,因为它返回的时候不知道我们要用什么,当我们不知道这个指针需要对接到什么指针的时候,就可以用void *代替,等到实际用的时候,自己定义即可。
int *a; 比如先定义一个变量。
a=malloc(3*4); 对a赋值,那就等于a拿到了这个申请内存的首地址。
*a=0x33; 引用指针对a初始化。
效果是一样的,所以这就是数组与指针的关系,数组就是指针的另一种形态。
对于一些复杂的指针,包括二级指针、函数指针、结构体指针,就不介绍了,用的不是很多。
指针用的最多的就是以下的几个:
传递参数和传递返回值。
注意事项:
- 在对指针取内容之前,一定要确保指针指在了合法位置,否则将会导致程序出现不可预知的错误;也就是上面的讲的当指针指向一个单独的变量的时候,再让这个指针加加,那他就指向了一个非法的区域。数据也有越界,数组越界和指针指向非法位置是一样的性质。还有一种不正确的形式是定义完一个指针后,不对其赋初值,直接引用指针是有问题的,因为不赋初值,直接引用p,p的值是不确定的,那是不是就引用到不确定的地址上去了,那直接引用p是有问题的。
- 同级指针之间才能相互赋值,跨级赋值将会导致编译器报错或警告。
正常情况下,同级指针才能相互赋值,变量可以说是0级指针。
当我们想把一个变量赋值给一个指针,那就要对这个变量取地址,因为变量取地址他就是指针,变量取地址才能给指针赋值;如果指针赋值给一个变量的话,最好加一个指针取内容,这样才能赋值。二级指针与指针间赋值同理。
下图直接跨级赋值,将指针p赋值给变量a,可以看到下面会有警告,说将指针赋值给一个整形,而没有强制转换。
a=p*;的话就没有问题了,但是由于p没有值,所以不太合理。
反过来p=a;是不行的,会有警告,说把整形的赋值给一个指针。如果真的要赋值的话就要给一个取地址的符号,p=&a;。
下一部分是指针的特色应用,实际的应用例子。如果感兴趣的话可以看一下。