C语言--14.指针

在我们整个C语言的学习中,有一个最重要的概念,相信大家都已经有所耳闻,便是指针,在本章我们将详细对指针进行一个介绍,帮助大家了解,认识什么是指针,让我们一起开启对于指针的学习吧!

在本章中,我们会回答指针的如下问题

1. 指针是什么
2. 指针和指针类型
3. 野指针
4. 指针运算
5. 指针和数组
6. 二级指针
7. 指针数组

指针是什么

在计算机科学中, 指针 Pointer )是编程语言中的一个对象,利用地址,它的值直接指向
points to )存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为 指针 。意思是通过它能找到以它为地址
内存单元

让我们来看一下指针的官方解释,重点解释为:将地址形象化的称为“指针”,所以我们可以将指针理解成就是地址,这是一定没问题的!同时,地址也是指针!

那么地址到底是什么呢?让我们进入到内存中来剖析一下,在我们的认知中,在我们32位平台下,CPU访问的基本单位是,字节

在我们的32位机器CPU中,便会存在32根地址总线,而对于我们计算机而言,总线只能表达两态的概念,所以我们32根线可以表示2^32次方种排列,也就是可以表示2^32种不同的状态,所以当我们对总线,二进制码进行编址的时候,便可以一共排列2^32种组合,也就是最多存在2^32个不同的地址单元,所以,我们在使用时看到的地址,就是这2^32个不同的地址单元转化成16进制而得到的结果,且这些地址是系统自带的,无法被修改,那么又因为我们CPU的最小访问单元是字节,所以此时边有一个问题出现了,就是,32位机器下最多有多少内存大小呢,通过一系列计算我们可以得到,2^32*1字节的大小等于2^2*2^10*2^10字节,我们对其进行简单的换算便可知道其为4GB大小,所以我们可以得到,32位机器,最多会有4个GB的内存,有2^32个地址,又因为2^32种排列,可以用4字节来表示,所以我们看到的地址都是4字节大小的

那么我们64位机器呢?事实上,64位机器内存大小为4GB*2^32大小,这个大小非常的大,所以我们在购置电脑手机时一般配置8GB,16GB,32GB等就足够了,理论上64位机器的内存地址个数是个天文数字,但实际上我们在日常运用是时不上那么多,所以,跟上面32位机器一样,8字节便可以表示4GB*2^32种排列方式,所以地址大小为8字节

当我们有了内存硬件层面的概念之后,我们就可以对其进行进一步学习了,那么我们再来看一个概念

指针
指针是个变量,存放内存单元的地址(编号)。

看到这个概念,是不是有了些许疑惑,怎么跟上面刚刚说过的不一样呢?

事实上,指针是个变量,这个概念我们应该拓展一下为,指针变量是个变量,在我们指针的使用中,许多时候是将指针放到左值的位置充当变量来使用的,但是在我们教材中又并没有指针变量这个概念(这个应该是历史遗留问题)所以我们将指针变量与指针统称为指针,具体其到底是哪一个,取决于其所在的位置左值还是右值

我们来观察一下这个代码

    int a = 10;
    //同样是a,但是处在不同位置,代表的含义是不同的!
    //a内容:右值
    //a空间:左值
	int b = a;
	a = 20;

此时我们对其进行翻译,首先定义整型变量a,给a开辟一个4字节大小的内存空间,将10放入a的空间中;其次同样再定义整型变量b,给b开辟一个4字节大小的空间,将a的内容10,放入b变量的空间中;最后将20放入a变量的空间中。

这便是我们对上述代码的剖析,可以发现,当变量处在表达式左右两边不同位置时,所代表的含义也是不同的,左值一般代表空间,右值一般代表内容

    int a = 10;
	int b = 20;
	int *p = &a;
	int *q = p;//右值,&a指针
	p = &b;//左值,变量

下面我们引入指针对这个概念进行理解,首先定义整型变量a,将10赋给a,定义整型变量b,将20赋给b,其次定义一个整形指针变量*p,开辟4字节大小,将a的地址,放入这个指针变量中,而后定义一个指针变量*q,将p的内容(a的地址)放入*q的空间中,最后将b的地址放入p的空间中,最后我们的*p是b的地址,q是a的地址,其实这段代码与上面的没有区别,仅仅只是将整型变量,换成了指针变量,但是我们在叫法上还称之为指针,同样的,当指针在左值的位置强调的是其变量,空间的性质,理解为指针变量,而当指针在右值的位置上时,强调的则是内容,地址数据。所以我们的教材才会既称指针为变量,也称指针为地址,就是因为他们所处的位置不同,起到的作用不同。

指针和指针类型

这里我们在讨论一下:指针的类型 我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类
型呢? 确切的说:有的。
char   * pc = NULL ;
int   * pi = NULL ;
short * ps = NULL ;
long   * pl = NULL ;
float * pf = NULL ;
double * pd = NULL ;

事实上,定义指针时就是采用type+*的方式来定义的,而前面的type则代表的就是指针的类型,其含义为什么类型的指针存储什么类型变量的地址

所有的指针在32位下都占用4个字节的大小

指针+-整数

我们来观察一下下面的代码

    int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int *p = &arr[0];
	printf("0x%x\n", arr);//数组首元素地址
	printf("0x%x\n", p);//数组首元素地址
	printf("0x%x\n", p+1);//将首元素+1步长(4)

在我们之前的学习中,单独数组名大多数情况下代表的都是首元素的地址,在上述代码中,p存储的也是首元素的地址,那么如果我们对其进行+1操作呢?结果发现,其地址变为原来+4(int型大小为4),加了一个指针类型的长度,可以理解为加了一个数组中元素大小的长度,若是将其更换为char类型,则会变为+1(char类型大小为1),所以我们的结论为:对指针+1其实是加上其指向类型的大小,同样的-也是对其减去类型步长大小

指针的解引用

在我们之前的学习中,对解引用的认识为,对指针的解引用,代表指针所指向的目标。这个结论是没有错的,我们来对其进行更深一步的理解

我们来观察下上述过程,首先将0x11223344赋给整型n,其次将n取地址转成char型赋给char*型指针pc,我们得出的是下面两个四字节大小的格子,他们中存储的都是n的地址,不过一个是char型,一个是int型,而后,我们将他们分别解引用,0赋给他们的解引用,依照我们之前对他们的理解,对指针解引用,代表的就是指针所指向的目标,所以我们的*pi=0x11223344,而后将0赋给*pi,所以我们的int n4个字节空间(红色格子)就变成了0,这个没问题,但是我们再来看一下*pc,其类型为char*类型,对char*类型的指针进行解引用操作,我们可以得到的是对整型4字节int n空间的第一个字节的访问,也就是图上的蓝格子,所以将*pc = 0;将0赋给解引用*pc,这个操作实际上是将0赋给int n的第一个也是char*指针最多能访问到的字节空间,最终得到的结果其实是0x00223344,举个例子,当我们指针类型为int型时,对其解引用我们看到的是4字节大小,指针类型为char时,对其解引用我们能看到的就是1字节大小,short型则是2字节大小,等等,所以我们可以得出结论:对指针解引用,代表的是指针所能看到的空间字节数,代表能够访问sizeof(type)个字节大小的空间,这里的类型指的是其自身指针的类型

在我们探究指针解引用时,我们发现了另一个问题,我们来看一下上述代码在内存中的实际储存

我们发现,当我们在内存中观察数据存储时,按照常理我们会想到0x11223344,11放在低地址,44放在高地址,这样依次排下来,然而实际情况却不是这样的,实际是低地址存储44,高地址存储11,依次排开,这个问题有一个专门的术语,叫做大小端问题。在我们数据中,存在高权值位与低权值位,比如123中,1的权值位最高,因为其为百位,3的权值位最低,因为其为个位,同理,我们刚才那组数据按照字节11 22 33 44 中,11的权值最大,44的权值最小,而我们的地址在编址的过程中又存在高地址与低地址之别(如上述的10到13),此时便出现了一个问题,是将高权值放入低地址,还是将高权值放入高地址,这一点并没有机会达成共识,所以我们不同的编译器厂商就自行规定了不同的规范,这样市面上就出现了两种不同的存储方式,大端存储与小端存储,大端存储便是低权值配高地址,小端存储则是低权值配低地址,而在我们的vs编译器中,采用的是小端存储方式,所以我们才会见到44,这个低权值的数据存储在低地址中,这也就解释了为什么为什么内存存储并没有像我们想的那样依次存储,事实上,我们大部分用的平台都是小端存储,大小端存储方式的记忆可以简化为小小小,所对应的就是低地址存低权值则为小端,反之则为大端,注意要以字节为单位

下来我们举一个例子来对大小端问题进行巩固

    int n = 0x11223344;
	short *ps = (short*)&n;
	printf("0x%x\n",*ps);//0x3344

我们来看一下这段代码,在我们认知里结果应该是什么呢?实际上结果为0x3344,这是因为当我们存储时候会考虑大小端的问题,取出的时候同样会考虑大小端问题,又因为取出的是两个字节大小,所以结果便为0x3344,过程为:44 33 22 11short类型取出44 33,而后输出时放入函数重新考虑大小端,44为大权值,33为小权值,所以小权值放低地址,大权值高地址,最后则为33 44

野指针(悬垂指针)

首先我们来认识一下什么是野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

此时我们定义了一个int 型指针让其随意指向了一个地址为0x1234的地方,结果在运行时出现了报错,这个int *p便是我们的野指针,因为其指向空间任意,不是我们自行开辟的,那么如何解决野指针问题呢?

事实上我们只需要将其指向NULL(0地址空间)即可,虽然指向NULL依旧会出现报错,但是这个报错只是在提醒其指向了0号地址空间NULL,有了确定的指向空间,并不会使其指向任意空间,造成其他数据丢失,程序崩溃或者更加严重的后果

下面我们来举一个引起野指针的例子

    int arr[10] = { 0 };//创建一个大小为10的int型数组
	int *p = arr;//对arr[0]进行解引用
	int i = 0;
	for (i = 0; i <= 11; i++)//循环11次
	{
		*(p++) = i;//将剩下的数组元素以及其后一个单位的地址进行解引用
	}

我们可以观察到,数组一共只有10个元素,但是我们循环了11次,最后一个指针访问越界了,此时这个指针就是野指针

那么我们如何来规避野指针呢

1.指针初始化

2.小心指针越界

3.指针指向空间即使置NULL

4.指针使用之前检查其有效性

指针运算

指针运算一共可以被分为3类

指针 +- 整数
指针 - 指针
指针的关系运算

1.指针+-整数

在之前我们就已经对这个问题有了一些认识,当指针+1时相当于其加上其指向类型的大小(步长)

2.指针-指针

 我们在认识这个问题时我们来看一段代码

int my_strlen(char *str)
{
	if (NULL == str){
		return 0;
	}
	char *end = str;

	while (*end){//当循环结束时end指向/0
		end++;
	}
	return end - str;//返回end与str指针的差值
}

当我们用这段代码去测量字符串长度时,我们是可以得到字符串长度的,指针相减得到中间元素个数的差值

但是我们再来观察另一段代码

int a[10] = { 0 };
	int *p = &a[1];
	int *q = &a[9];
	printf("%d\n", q - p);//8
	

按照我们常理来想应该是多少呢?我们一定会想,9号元素地址减去1号元素地址,中间有8个元素,而每个元素都是int型,占用4个字节,所以我们最后会得到32这个答案

但是结果并不是的,结果是8,为其元素个数

 注意:指针在只想元素时一定是之想起地址数值最小的那个地址,这里我们认为是在最左侧

所以我们可以对其进行总结:两个指针相减得到的答案是两个指针之间所经历的元素个数(并不是所跨越的字节个数)

但是这里的元素,不由真正中间差的元素个数说了算,而由指针类型说了算,我们对上述代码进行修改

int a[10] = { 0 };
	short *p = (short)&a[1];
	short *q = &a[9];
	printf("%d\n", q - p);//16
	

此时因为short型大小为2,我们指针所指向的地址没有改变,但是元素大小减半,所以我们的元素个数会*2

3.指针的比较

在我们之前学习数组时,我们也简单提到过指针可以对数组进行操作,那么我们来看一下下面这种情景

 当我们拿3种方式来对数组进行初始化时,第一种是从前往后遍历终止条件为小于不存在的第6号values[N],所以可以对前面5个数组元素进行初始化,第二种方式是从后往前,大于第0号元素,再通过--操作来对第0号元素进行初始化。而第三种则是先将中括号-1,再让其大于等于第0号元素,这时就会出现当我们从后往前进行初始化时会遍历到values[-1]的位置,让其与values[0]进行比较,从而终止循环,在这里我们不推荐第三种做法,因为我们不确定其前一个位置的地址是否被使用,虽然在大部分编译器内是可行的,但是我们还是避免这样写,因为标准不保证他可行

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的那个内存位置的指针进行比较。

指针和数组

首先我们需要明确的一点是:指针和数组没有任何关系

虽然我们的数组与指针在操作上有许多的相似之处,但是数组与指针没有任何关系

int arr[10];
	//for (int i = 0; i < 10; i++){
	//	//arr[i] = i ;//下标访问方式
	//	*(arr + i) = i;//



	int *p = arr;
	for (int i = 0; i < 10;i++){
		//*(p + i) = i;//指针访问方式
		p[i] = i;//数组的中括号访问方式对于指针同样适用
	}

我们可以发现,数组与指针在访问连续空间时方式是一样的,而为什么我们在操作数组时和指针一样呢,那是因为数组在传参过程中需要发生降维,降维成只有4字节大小的指针,而在函数中若不统一他们的操作方式就需要在外层使用数组操作方式,内层函数中使用指针的操作方式,这很不方便,程序员们需要在内层与外层对同一个东西进行两种操作方式,所以为了方便考虑,在设计之初就对数组与指针在操作上的设计统一了,都可以用中括号或解引用的方式访问

那么我们如何来证明数组与指针并不是一个东西呢,让我们来看一下下面这人张图

 上面是我们对于数组的1号元素的两种访问方式,一种是通过首元素地址+1解引用来访问,而另一种是对指针+1解引用操作的,而我们这两种操作方式,对于p而言,其为指针,有变量的性质,存储的是首元素地址,所以我们在调用其时需要进行读取数据,而我们进行读取数据时,需要进行从内存中读取到寄存器等操作,但是对于直接使用数组下标+1时就不存在这种问题,没有读取变量这一过程,所以我们这两种数组访问方式来看,指针的效率是相对于下标而言更低的

二级指针

对于二级指针而言,其与一级指针的概念都一样,都是地址,大小也为4,只不过二级指针中存的是一级指针的地址,也就是所谓的指针的指针

 让我们通过下述代码来对二级指针进行更清楚地描述

    int a = 10;
	int *p = &a;
	int **pp = &p;
	p = 10;//将p指向地址为10的一个指针变量
	*p = 10;//将a赋为10
	pp = 10; //将pp指向地址为10的一个指针变量
	*pp = 10;//将10赋给p
	**pp = 10;//将a赋为10

 对于上面这几种情况我们只需要记住,对指针解引用代表的是其所指向的目标

指针数组

指针数组是数组,只不过数组内的元素类型是指针

其操作都和数组是一样的,比如int *arr[5]进行传参时传的便是int **arr[5]

而数组指针是指针,是用来描述数组各个元素地址的指针

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值