目录
一、关于指针
二、指针类型
1、整型指针的访问权限说明:
2、字符指针的访问权限说明:
3、指针的类型决定向前或向后一步走了多大距离
三、野指针相关知识
1、野指针的成因
①指针未初始化
②指针的越界访问
③指针所指向的空间释放了
2、如何规避野指针
①指针要初始化
②要注意指针越界的问题
③指针所指向的空间及时置NULL
④避免返回局部变量的地址
⑤指针使用之前检查有效性
四、常量指针和指针常量
1、常量指针
2、指针常量
五、有关指针的运算
1、指针+-整数
2、指针-指针
3、指针的关系运算
六、字符指针
常量字符串(相关知识)
七、指针数组
数组名的含义
八、数组指针
九、数组参数,指针参数
1、一维数组传参
2、二维数组传参
3、一级指针传参
4、二级指针传参
十、函数指针
函数指针重命名
十一、函数指针数组
一、关于指针
首先内存会划分为小的内存单元,每个内存单元都有一个编号,这个编号就被称为地址,我们把地址也叫指针,如下图所示:
注意注意:指针指的是地址,但是我们口语中的指针通常指的是指针变量
指针变量就是用来存放地址的变量
我们是可以通过修改指针变量的值来修改原数值,如下:
原本a的值是5,通过指针变量pa,解引用也可以改变a的值
关于指针的大小:在32位平台是4个字节大小,在64位平台是8个字节大小,并不会因为指针类型的改变而改变!下方是VS2019的32位平台环境下的演示:
观察可知,无论是整型指针还是字符指针在32位平台下内存大小都是4个字节
注意:32位平台即是32个bit组成的,所以32个bit=4个byte,因此在32位平台指针是4个字节的大小,64位也是相同的道理
二、指针类型
指针类型决定了,指针在被解引用的时候所访问的权限,例如:
整型指针解引用会访问4个字节
字符指针解引用会访问1个字节
下面来具体证明这两个类型的指针的访问权限:
1、整型指针的访问权限说明:
a为16进制0x11223344,在监视页面观察a与*pa相同,都是数值;&a与pa相同,都是地址
观察此时内存中a的存储
输入a的地址,发现内存中存储为44 33 22 11,这里涉及到了我们前面所讲到的大小端存储,不知道的小伙伴可以到“C语言之数据的存储”那篇博客中学习
我们VS中用的是小端存储,所以说为44 33 22 11,接着运行到*pa=0后,我们再观察内存可知:
a的值被更改为了00 00 00 00,4个字节的值都被改了
2、字符指针的访问权限说明:
同样按照刚才的顺序,如下图:
代码部分其他都不变,只变了指针的类型
监视和内存部分一样
当代码运行*pa=0后
我们发现,字符指针类型,解引用只能改变2个16进制位,即8个bit位,即1个byte
上面两个例子证明了不同的指针访问权限是不同的
3、指针的类型决定向前或向后一步走了多大距离
具体意思就是说指针变量加或减一,它的地址变化的量,如下所示:
分别定义了整型指针变量pa和字符指针变量pb, 分别用%p打印地址,观察运行结果可知:
pa和pb地址相同,因为pa和pb都是指向a的地址
而pa+1和pb+1则大大不同,pa+1相对于pa加了4,而pb+1相对于pb只加了1,从这个例子中可以看出指针的类型也决定了,指针向前或向后走一步能够走多大的距离。
三、野指针相关知识
野指针就是指针指向位置是不可知的!
1、野指针的成因
①指针未初始化
这种问题非常常见,具体看如下代码;
运行后报错也会显示这种错误
代码int* p没有初始化,所以p变量中存的地址并没有指向我们当前程序的空间,而是指向内存中随机的空间,因此要*p要访问这块内存空间肯定是出错的!
这里的p就是野指针
②指针的越界访问
这个问题可以说是非常常见的了,我们平常在遍历数组中,一不注意就会遇到这样的问题:
我们设定数组arr,让指针变量p等于数组首元素的地址,数组本身有5个元素,但是在for循环中却循环6次,i分别是0 1 2 3 4 5,本身i是0~4的,循环时多了个5,导致非法访问内存,因此第六个数打印出来是随机数,这里也是野指针的问题了
③指针所指向的空间释放了
这个问题也是经常遇到的问题啦,具体看下方例子:
test函数的返回值是x的地址,main函数中用指针变量p接收x的地址,但是x变量进入test函数创建,而出了test函数会销毁,这时再改变*p的值,即使用x的地址,则是非法访问内存了,也会造成野指针的问题
2、如何规避野指针
①指针要初始化
比如上方的例子中,不能直接int* p,必须要初始化,int a = 10;int* p = &a;从而规避野指针问题
②要注意指针越界的问题
在我们使用数组时,一定要注意数组的元素个数以及我们所循环的次数,避免粗心而导致越界访问
③指针所指向的空间及时置NULL
在我们不使用指针变量p时,int* p = NULL;置为空,在接下来想要使用p时,用if语句:
if(p != NULL) ......
能够很好地避免野指针
④避免返回局部变量的地址
就像上方野指针成因的第三条所举的例子,避免返回局部变量的地址
⑤指针使用之前检查有效性
像if(p != NULL) ......就是在检查指针的有效性
四、常量指针和指针常量
常量指针和指针常量在题目中遇到的可算是非常多啦,它们都与const这个关键字有关,下面举三个例子,看看大家能不能分清其中的区别:
const int *pa = &a;
int const *pa = &a;
int *const pa = &a;
1、常量指针
常量指针的概念是:指针所指空间的值不能发生改变,不能通过指针解引用修改指针所指向空间的值,但是指针的指向是可以改的
2、指针常量
指针常量的概念是:指针本身就是一个常量,即指针的指向不能发生改变,但是指针所指向空间的值是可以发生改变的,可以通过解引用改变指针所指向空间的值
具体怎么更好的理解呢?
大家先看常量指针这个名字,以指针结尾,所以是具有指针的性质的,因此可以改变指向,又因为是常量指针,所以指向的值是常量的,即是不能改变的
再看指针常量这个名字,以常量结尾,应该具有常量的一些性质,所以自然不能改变指针的指向,只能修改指针所指向的值啦
那么const究竟加到代码的哪个位置才叫常量指针,加到哪个位置才叫指针常量呢?
说一个小窍门可以很快的帮助大家记住:
const如果在*的左边,那就是常量指针
const如果在*的右边,那就是指针常量
至于为什么要这样记呢,可以思考一下,*在指针中有解引用的作用,那么const在*左边,可以理解为const修饰*,即不能修改指针所指向空间的值,但是指针的指向确是没有限制的
那么const在*右边,修饰的就是指针变量啦,即指针变量不能改变它的指向,但是指针所指向空间的值不受限制
那么上面的三个例子就很明显有答案了,只需要看const修饰的是*还是指针变量即可
常量指针:const int *pa = &a;、int const *pa = &a;
指针常量:int *const pa = &a;
五、有关指针的运算
1、指针+-整数
举个例子简单说明指针加减整数的情况:
指针变量p是数组第一个元素的地址,用for循环打印数组各个元素,*的优先级高于++,所以每次执行*p打印完数组中元素后,p++再指向数组下一个元素,从而循环5次打印出数组中5个元素
2、指针-指针
指针-指针的操作得到的是指针和指针之间元素的个数,当然前提是两个指针必须指向同一块空间,下面看例子;
具体画图进行如下讲解:
如上图所示表示数组中5个元素在内存中的存储,其中p1指向第一个元素的地址,p2表示第五个元素的地址,代码中用p2-p1,就表示的是两个指针之间的元素个数,很清楚的看到是4个元素,所以打印出来结果为4
3、指针的关系运算
指针的关系运算就是指针之间进行大小的比较,从而实现某些功能,例如:
圈出来的即为指针的关系运算,下方所画的图可以清楚解释:
p刚开始是数组首元素的地址,接着通过p<&arr[5]的比较,满足则打印该地址下的元素,接着p++,进行下一轮的比较,直到不满足p<&arr[5]为止
这里可能有小伙伴要问了,上文不是说到不能越界吗,数组只有5个元素,下标最大是4,怎么可以用arr[5]呢,其实这里并没有越界,上文是使用了越界的地址,已经非法访问内存了,而这里只是将这个地址写出来,和指针变量p进行关系运算,并没有使用这个地址,所以不存在野指针的问题,请大家放心♪(^∇^*)
规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较(如上方例子),但是不允许与指向第一个元素之前的内存位置的指针相比较
六、字符指针
字符指针就是存字符的地址,就是将字符变量的地址放到字符指针里去,举例如下:
定义指针变量pa存a的地址,改变*pa的值,a也会随之改变
常量字符串(相关知识)
如果说有这样的代码:char* pa = "abcde";这里的"abcde"就是常量字符串,"abcde"是存储在常量区的,pa指向a的地址
常量字符串是不能被修改的的,如果说有人想写类似于*pa='w'的这种代码,程序是会崩溃的,所以说我们可以在前面加上const进行保护,写成const char* pa = "abcde"这种形式(这里就是常量指针的类型啦)
并且这个代码并不是将"abcde"存到pa中,而是说将a的地址存到指针变量pa中,下面的例子能更加清楚的解释这个知识:
解引用pa,打印出来发现确实是字符a,接着pa+1,理应就是指向字符b了,解引用并打印出来也确实是字符b的值,如果想打印整个字符串,那么就直接使用%s,pa就能打印出来了
下面有一道非常经典的例题:大家可以思考一下,p1和p2,p3和p4是否相等
答案是:p1和p2相等,p3和p4不相等
通过上面内容可知,"abcde"是常量字符串,存储在常量区,所以p1和p2都是指向这个常量字符串的,都存储的是a的地址,所以p1和p2是相等的
而p3和p4分别是两个数组的初始化,所以p3和p4分别表示两个数组的首元素的地址,所以p3和p4是不同的
七、指针数组
说起指针数组,大家可能听得比较少,但是说起整型数组,字符数组这样的词汇时,大家可能就会听的比较多了,那么到底什么是指针数组呢
先说说整型数组和字符数组
整型数组:字面意思可以看出,整型数组就是存储整型的数组,例如int arr[5]={1,2,3,4,5},arr数组就是一个整型数组,数组有5个元素,数组中每个元素的类型是int
字符数组也和整型数组一样,以此类推了
那么说到这里,指针数组的定义就很简单了,就是存储指针的数组了,例如int* arr[3]={&a,&b,&c},数组有3个元素,数组中每个元素都是指针,每个元素的类型都是int*,这样的数组就叫做指针数组
下面举个案例能让大家更明白指针数组实际中的应用:
我们首先创建了3个整型数组arr1,arr2,arr3,接着创建一个指针数组arr,存储这三个整型数组首元素的地址(因为数组名就是数组首元素的地址),我们将3个整型数组的首元素地址存进指针数组arr中,那么想要表示整型数组中的元素时,相当于创建了一个二维数组,下图可以清楚表示:
arr数组中每一个元素都是int*类型,那么arr[i]就是表示arr数组中的元素,也就是arr数组中的3个整型数组的首元素地址,arr[i][j]就是3个整型数组中的下标为j的整型元素,所以这个指针数组的应用相当于一个二维数组
另一种表示方法是*(arr[i]+j),arr[i]就是表示arr数组中的元素,即3个整型数组的首元素地址,再+j就是指从首元素地址向后移j个元素的地址,再解引用打印出来,也是整型数组的元素
下面举一个arr1数组的例子,大家就会更清楚了
这便是代码中两种打印方式的解释,相信大家都清楚其中道理啦!
数组名的含义
说到了数组,那么我们不得不说一说数组名的含义
一般来说数组名是数组首元素的地址
但是也有2个例外:
1、sizeof(数组名),这种情况数组名不是数组首元素的地址,这时候的数组名表示整个数组,计算的是整个数组的大小
2、&数组名,这种情况数组名也不是首元素的地址,也表示整个数组的地址,取出的是整个数组的地址
下面画图帮助大家理解
由图可知,&arr[0]、arr、&arr他们的数值都是数组首元素的地址,但是代表的含义却并不相同,&arr[0]和arr这两个若是+1,则跳过一个整型元素,指向下一个整型元素的地址,而&arr若是+1,则是跳过一个数组,指向元素8的下一个位置的地址
通过代码大家可以清楚观察到刚刚得出的结论
以上便是要补充的关于数组名的含义的知识点啦♪(^∇^*)
八、数组指针
说完指针数组,现在我们来说一说数组指针,指针数组是数组,那么数组指针就是指针了
以前我们了解过整型指针,int* p=&a,是指向整型的指针,存放整型变量的地址;而字符指针是指向字符的指针,存放字符变量的地址;那么数组指针自然就是指向数组的指针了
那么接下来举个例子:
int* p1[3];
int (*p2)[3];
上方这两个例子,哪个是数组指针,哪个又是指针数组呢?
大家可以先观察p1和p2,是以前我们所学的数组名的地方,先看第一个int *p1[3];因为[]的优先级高于*p1,遇到方括号会先与方括号结合,因此这是一个有3个元素的数组,而观察p1前面的int*,就可以知道这个数组中的每一个元素的类型都是int*,所以这是一个指针数组
在看第二个int(*p2)[3];p2先与*结合,p2是一个指针变量,因此这是一个指针,指向大小为3的数组,每个元素的类型为int,所以这是一个数组指针
首先对于整型指针,int a = 5; int* pa = &a,给出一个整型变量a,取a的地址放到整型指针pa里面,这里的指针变量pa的类型是int*,可以认为是int* pa去掉pa这个变量名后,剩下的就是这个指针的类型
那么同理,int arr[3]={0}; int (*parr) [3] = &arr ;给出一个整型数组arr,里面有3个元素,那么取数组的地址就自然应该放在数组指针parr里面,那么问题来了,数组指针parr的类型是什么呢?很简单,也是int (*parr) [3]去掉指针变量名字即可得到,即int (*) [3],根据类型可以得到,这样的一个指针,指向一个数组,数组有3个元素,每个元素的类型是int
下面可以举个例子具体说明:
根据上面所说的,pa是存储a变量的地址,类型是int*,所以打印地址时pa+1地址由00A8FB54增加到了00A8FB58,增加了4个byte的大小(一个整型的大小),而parr是存储数组arr的地址,类型是int (*) [3],所以理所当然,parr+1的地址相较于parr的地址由00A8FB34变为了00A8FB40,增加了12个byte的大小(三个整型的大小)因为这个指针变量指向的数组有三个元素,每个元素都是int类型,因此parr+1跳过了3个整型变量的地址。
九、数组参数,指针参数
在代码中需要将数组和指针传给函数,涉及到了数组参数,指针参数的概念
1、一维数组传参
有一维数组arr1和arr2,给test1和test2传参,那么test的形参应该如何写呢
因为传数组名,相当于传数组首元素的地址,所以形参既可以写成数组的形式,也可以写成指针的形式
arr1数组传参:
数组的形式:void test1(int arr1[])或void test1(int arr1[5])
每个元素类型是int,所以指针是int*
指针的形式:void test1(int* p1)
arr2数组传参:
数组的形式:void test2(int* arr2[])或void test2(int* arr2[15])
每个元素类型是int*,所以指针是int**
指针的形式:void test2(int** p2)
2、二维数组传参
有一维数组传参的基础,二维数组传参也有两种形式
注意二维数组传参,行可以省略,列不能省略,因为列如果省略就不知道每一列具体几个,则会导致内存中排列也会有问题,因为内存中是第一行和第二行连续存储的
数组的形式:void test (int arr[][3]) 或 void test (int arr[2][3])
数组名是数组首元素的地址,二维数组首元素地址就是第一行的一维数组的地址,所以第一行数组的地址就是一个数组指针:int(*)[3],形参应该是数组指针p:int(*p)[3]
指针的形式:void test(int(*p)[3])
3、一级指针传参
一级指针传参,传一级指针,那就用一级指针接收
同样也可以反推,如果test的参数是int* ptr,那么函数可以接收以下三种参数
①test(arr)②test(p)③有个int a = 0;可以传a的地址test(&a)
4、二级指针传参
二级指针传参,传二级指针,那就用二级指针接收
那么同样的,如果test的参数是int** ptr,那么函数可以接收以下三种参数
一和二容易理解,看这个第三个,arr数组有5个元素,每个元素都是int*类型,传入arr相当于传入数组首元素的地址,而首元素类型是int*,那么int*的地址就是int**,也满足要求
十、函数指针
函数指针就是指向函数的指针,下面举函数指针例子
这里的p就是函数指针
首先p和*在括号中先结合,表明p是指针,后面括号中是该函数的的参数类型int,int
这里一样,p是函数指针,但这里是print而不是&print,是因为这两种方式表示的意义都相同
调用时,上面两种都可以调到函数
函数指针重命名
如果一个函数指针类型是int (*)(int,char),觉得太长太复杂,可以用typedef重命名,但这里的重命名和以前的命名格式不同,所起的名字是要放在*号后面的,即:typedef int (*p)(int,char),代码意思就是给函数指针类型int (*)(int,char)重新起名叫p
十一、函数指针数组
函数指针数组是一个数组,数组中存的是函数指针,如下所示:
int(*ptr[3])(int, int)就是函数指针数组的运用,ptr是函数指针数组,去掉数组名ptr和[3],可知数组中每个元素是int(*)(int, int)的函数指针,所以称之为函数指针数组
关于指针的内容就是这些了,有兴趣可以多加研究呦O(∩_∩)O