【逐步剖C】-第五章-指针初阶

一、指针的基本知识

1. 预备知识:

(1)内存的简单概念:

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行。
为了有效的使用内存,就把内存划分成一个个小的内存单元,经过仔细的计算和权衡规定每个内存单元的大小是1个字节。
为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

如下是参考图:
在这里插入图片描述

(2)地址编号方法的简单说明:
对于32位的机器,有32根地址线,那么每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001

11111111 11111111 11111111 11111111

那么这里总共就有2的32次方个地址编号。
每个地址标识一个字节,那我们就可以给 (232Byte = 232/1024 KB =
232/1024/1024 MB = 232/1024/1024/1024 GB = 4GB) 4G的空间进行编址。(PS:8个比特位(bit)等于一个字节(byte),1024个字节等于1KB;1024KB等于1MB;1MB等于1GB……)
以上对于64位机同理。
那么我们就可以得出:
在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储(因为8个比特位等于一个字节,而一个二进制位就对应着一个比特位,故一个地址所对应的一个二进制序列总共有32个比特位),所以一个指针变量的大小就应该是4个字节。
那么如果在64位机器上,有64根地址线,那一个指针变量的大小自然就需要8个字节来存放一个地址

2. 理解指针的2个要点:

(1)指针本质上是内存单元中一个最小的编号,也就是地址
(2)平时我们口头所说的指针通常指的是指针变量(用来存放内存地址的变量)
也就是说,指针本质上就是地址,那么如果要存储一个地址/指针,相应地就需要一个指针变量。就和存储整型是一个道理:若需要存储一个整型,那我们就需要创建一个int 型变量。

3. 指针变量的基本操作:

我们可以通过取地址操作符 ‘ & ’ 来取出某个变量的地址,然后存入指针变量当中;
我们可以通过解引用操作符 ‘ * ’ ,借助指针变量中所存储的地址而找到对应的变量,并且能对变量内容进行修改。
如下面这段代码:

#include <stdio.h>
int main()
{
	int a = 10;//在内存中开辟一块空间
	int *p = &a;//这里我们对变量a使用&操作符取出它的地址,存入指针变量当中
	*p = 20;//修改变量a中的内容
	printf("%d",a);
	return 0;
}

运行结果:
在这里插入图片描述
解释: 代码中的p就是我们所说的指针变量,可以看到,创建它的方式就为int *,可以这么理解:
(1)‘ * ’ 说明p是一个指针变量
(2)int 说明p指向的对象是int型的(关于指针变量类型的意义下面会详细叙述)
而下面的语句*p = 20;中的‘ * ’ 为解引用操作, *p就是通过p所存储的地址找到a,且对a的值进行了修改。
可以发现,指针的使用无非就是存址、寻址,这其实和我们日常生活中通过地址去找一个人是非常类似的。如:刚到学校,你想找好朋友聊天,但你不知道他住在哪个宿舍,于是你发信息询问,他告诉了你相应的宿舍楼号和宿舍门牌号,那么自然你就可以通过他提供的这些信息找到他本人。 对应到代码中,语句int *p = &a;其实就是你记住他宿舍楼号、门牌号的过程;语句*p = 20;就是你通过地址信息找到他的过程。

  • 补充说明:
    因为这里a是一个整型变量,占用4个字节,而每个字节都有对应的地址编号,而我们对a取地址其实取出的是第一个字节的地址(较小的地址
    如下参考图:
    在这里插入图片描述
    我们取出a的地址就为图中的0x0012FF40(注:这里a的地址是便于说明而假设的一个值,编译器每次运行a的地址会不同)。
    也可在编译器调试验证:
    验证代码:
#include <stdio.h>
int main()
{
	int a = 10;
	int* p = &a;
	printf("%p", &a); //通过%p来打印a的地址
	return 0;
}

调试过程截图:
在这里插入图片描述
打印的a的地址,如图中绿色箭头所指;而图中红框部分其实就是a的地址,十六进制的00 00 00 0a 转换为十进制其实就是10。由此我们知道,对变量进行取地址操作本质上取出的是其第一个字节的地址。

二、指针和指针类型

指针变量和其他变量一样都是有类型的。如上面所展示的代码中指针类型就是 “ int* ”型的,它表示该指针指向 int 型变量的地址,那么同理我们可以得到:

char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL

代码中的NULL表示空指针,其实可以理解为0。
这样我们就知道了定义指针的具体方式:类型 + ‘ * ’ 号
对应地,char* 类型的指针是为了存放 char 类型变量的地址;short* 类型的指针是为了存放 short 类型变量的地址……
那么这些指针除了存放相应类型的地址之外,还有什么其他的意义呢?

1. 指针加减整数

看下面这段代码:

#include <stdio.h>

int main()
{
	int n = 10;
	char *pc = (char*)&n;
	int *pi = &n;
	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	printf("%p\n", pi);
	printf("%p\n", pi+1);
	return 0;
}

程序输出结果:
在这里插入图片描述

  • 解释: 这里的指针变量pcpi中存储的都是n变量的地址(n变量为整型,将其地址存入char* 类型的指针时需进行类型转换),这一点从输出结果的第二行第四行与第一行对比也得以验证。而不同的是,char * 类型的指针加1后,地址与加1前的地址相差了一个字节;int* 类型的指针加1后,地址与加一前的地址相差了四个字节原因是:存储一个char类型的变量,需要一个字节的内存空间;存储一个int类型的变量,需要四个字节的空间,其他类型的变量同理。
    不同类型变量对应的内存空间字节数如下图:

在这里插入图片描述
总结: 指针的类型决定了指针向前(减整数)或者向后(加整数)走一步有多大(距离)

2. 指针的解引用:

在第一部分指针的基础知识中,我们说到,解引用操作可以通过指针变量中所存储的地址而找到对应的变量,并且能对变量内容进行修改。其实这个 “修改”的“幅度”就由由指针的类型决定
下面我们通过调试来展示这个修改过程:

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char *pc = (char *)&n;
	int *pi = &n;
	*pc = 0; 
	//*pi = 0; 两次调试,一次执行一条语句,观察内存情况
	return 0;
}

初始状态的内存情况如下:
(红框中的内容就是变量n的地址)
在这里插入图片描述
第一次调试我们执行语句*pc = 0;,执行结果:
在这里插入图片描述
整型变量n地址中的第一个个字节的内容被修改
第二次调试我们执行语句*pi = 0;,执行结果:
在这里插入图片描述
整型变量n地址中四个字节的内容全部被修改
由此我们可以得到指针类型的另一个意义:
指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

三、指针与数组

1.通过指针访问数组

在学习数组的时候我们了解到,数组中元素的地址是从低到高顺序排列的,数组名通常代表着数组首元素的地址(有两种特殊情况,详情可见:【逐步剖C】第三章-数组),那么如果我们用一个指针变量获取了数组首元素的地址,那么我们就能借助这个指针找到数组中的所有元素。
看下面这段代码:

#include <stdio.h>
int main()
{
	int arr[] = {1,2,3,4,5,6,7,8,9,0};
	int *p = arr; //指针存放数组首元素的地址
	int len = sizeof(arr)/sizeof(arr[0]);//计算数组长度
	for(int i=0; i<len; i++)
	{
	printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
	}
	return 0;
}

运行结果:
在这里插入图片描述
可以看出,arr[0]的地址与p的地址相同;arr[1]的地址与p+1的地址相同……
所以,p+i其实计算的是数组arr下标为i的地址
由此配合上解引用操作,我们就可以直接通过指针来访问数组:

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int *p = arr; //指针存放数组首元素的地址
	int len = sizeof(arr) / sizeof(arr[0]);//计算数组长度
	int i = 0;
	for (i = 0; i<len; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

运行结果:
在这里插入图片描述

2. 知识补充:

由上面叙述我们可以得到下面的等价关系:
arr[2] 等价于 * (arr + 2) 等价于 * (2 +arr) 等价于 2[arr]
解释:上面我们说到,若arr[0]的地址与p的地址相同,p+i其实计算的是数组arr下标为i的地址,又因为数组名表示数组首元素的地址,即arr == &arr[0] == p,那么对p的操作,如上面的*(p+i)就可以等价于对arr的操作,即*(arr+i);再者,对于下标访问操作符’ [ ] '而言,数组名与下标不过是它的两个操作数,操作数的实际顺序并不影响其功能运作。当然只是作为知识扩充,实际上也没人会写成 2[arr]。
(PS:实际上编译器会将arr[2]转化为 * (arr+2),因为这样编译器才能进行“计算”)

四、指针的运算

1. 指针加减整数

如第二部分中所述,指针加(减)一个整数,指针就会相应地向后(前)走相应类型大小的距离。这里不再赘述

2. 指针减指针

规定:指针减指针的绝对值是两指针之间的元素个数
前提条件:两个指针指向同一块空间(类型相同)
如下代码:

#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int* p = arr; //指针存放数组首元素的地址
	int len = sizeof(arr) / sizeof(arr[0]);//计算数组长度
	int* q = arr + len;
	printf("%d\n", q - p);
	printf("%d", p - q);
	return 0;
}

运行结果:
在这里插入图片描述
示意参考图:
在这里插入图片描述
(PS:p和q之间的元素也包括q当前位置的那个元素,故总共有10个)

3. 指针的关系运算:

标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
对于以上可以这么理解:假设给定数组有10个元素,那么数组合法的下标范围就为0-9。若我们想与数组的第一个元素进行比较,那么只能向后越界找一块空间来作为比较的基准(注意只是作为比较基准而不能解引用,因为此时的指针为野指针,下面会详细讲述),如创建指针变量p = &arr[10],而不能向前越界,如p = &arr[-1]虽然下标是非法的,无法访问对应的内存空间内容,但还是可以取地址操作还是允许的。
这里用下面两段代码进一步解释:
其中N_VALUES为数组values所含元素个数;vp为对应类型的指针变量
代码的功能是:从数组最后一个元素开始,逐一把数组元素置为0。
(1)

for(vp = &values[N_VALUES]; vp > &values[0];)
{
	*--vp = 0;
}

指针变量vp初始化指向数组最后一个元素(下标为N_VALUES-1的后面一个位置,这里其实就是我们上面所说的向后越界作为比较的基准;
循环控制条件为vp > &values[0];
循环体中指针先自减1,再进行解引用,故不会发生越界错误,进行最后一次比较时,指针vp指向的位置与数组第一个元素(values[0])的地址相同,此时不满足循环条件,循环停止
(2)

for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
	*vp = 0;
}

指针变量vp初始化指向数组最后一个元素;
循环控制条件为vp >= &values[0];
这里也不会发生越界访问的错误,但与(1)不同的是,进行最后一次比较时,指针vp的指向的位置为数组第一个元素前一个元素位置的地址,此时不满足循环条件,循环停止。也就是我们上面所说的向前越界作为比较的基准

虽然实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

四、野指针

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

1. 野指针的成因

(1)指针为初始化
下面是错误的代码示范:

#include <stdio.h>
int main()
{
	int *p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

对于未初始化的指针进行解引用操作,就好像你在路上随便找了个陌生的房子就住进去了,既非法又危险。
(2)指针的越界访问
下面是错误的代码示范:

#include <stdio.h>
int main()
{
	int arr[10] = {0};
	int *p = arr;
	int i = 0;
	for(i=0; i<=11; i++)
	{
	//当指针指向的范围超出数组arr的范围时,p就是野指针,此时就不能进行解引用操作
		*(p++) = i;
	}
	return 0;
}

这就是我们在第三部分第3点所说的野指针问题。
(3)指针指向的空间释放
此部分在后面有关动态内存开辟的知识会有详细总结,这里做一个简单说明:
指针变量p原来指向一块内存空间,当内存空间还给操作系统后,p所指向的还是原来的地址,但此时再进行解引用操作就属于非法访问内存了。
就相当于你住酒店到期退房后,房间还是那个房间,但你在不续房费的情况下是不允许继续居住的
下面是错误的代码示范:

#include <stdio.h>
int* test()
{
	int a = 10;
	return &a;	//返回a的地址
}
int main()
{
	int *p = test();
	*p = 20;
	return 0;
}

由变量的生命周期可以知道,整型变量a在test函数结束后自动销毁,为其分配的内存空间也将还给系统,那么当指针变量p接受到函数返回的a的地址时,a的内存空间已经还给操作系统(被释放)了,故此时对其进行解引用操作就属于非法访问内存。

  • 补充:在编译器VS2022中,若在后面加上语句printf("%d\n", *p);确实也能打印出20,但这只是侥幸,编译器会给出警告。此时若在前面再加上一个打印语句printf("hehe");就不会打印出20了,因为此时具体函数栈帧中的内容已经发生变化。

2. 如何规避野指针

(1)进行指针初始化
即当创建一个指针变量但又暂时不确定其需要指向何处时,可直接初始化为NULL(空指针)。但需要注意的是,解引用空指针也是非法访问内存的行为。
(2)小心指针越界
(3)指针指向的空间释放后及时将相应指针变量置空
(4)避免返回局部变量的地址
如1中的第(3)点。
(5)指针使用之前检查其有效性

#include <stdio.h>
int main()
{
	int *p = NULL;
	//....
	int a = 10;
	p = &a;
	if(p != NULL)	//检查有效性
	{
		*p = 20;
	}
	return 0;
}

在使用指针之前可以通过语句if(p != NULL)来检查其有效性。这里需再强调一下第(3)点,当指针指向的空间被释放后,指针变量存储的还是原来的地址(非空),所以此时若不及时将其进行置空操作,就算检查其有效性也将会检查不出问题,再对其解引用就是非法访问内存了。

五、二级指针

实际上,指针变量也是变量,那么是变量就有地址,那指针变量的地址存放在哪里?
这就是 二级指针

1. 简单介绍

int a = 10;
int* pa = &a;
int** ppa = &pa;

如上三行代码表示的是:
a的地址存放在pa中,pa的地址存放在变量ppa中
pa是一级指针,而ppa就是二级指针
对于语句int** ppa = &pa;中的两颗 ‘ * ’ 可以这么理解:
第二颗 ‘ * ’ 表示ppa是一个指针;第一颗 ‘ * ’ 表示ppa指向的类型是整型指针

2. 关于二级指针的操作:

(1)*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa

int a = 10;
*ppa = &a;//等价于 pa = &a;

(2)**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用 操作: *pa ,那 找到的是 a

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

如下代码:

#include <stdio.h>
int main()
{
		int a = 10;
		int * pa = &a;
		int* * ppa = &pa;
		**ppa = 30;
		printf("%d\n", **ppa);
		printf("%d\n", *pa);
		printf("%d\n", a);
}

运行结果:
在这里插入图片描述

示意图参考:
在这里插入图片描述

六、指针数组

1. 指针数组的概念

同整型数组、字符数组,指针数组就是用来存放指针的数组,换句话说,该数组中的每个元素都是一个指针。如:int* arr[3]就表示arr是一个含三个元素的指针数组,每个元素都是一个 int*(整型)指针

2. 指针数组的运用

使用一维数组模拟二维数组:
代码如下:

#include <stdio.h>
int main()
//假设模仿一个三行三列的数组
{
	int a[] = { 1, 2, 3 };
	int b[] = { 4, 5 ,6 };
	int c[] = { 7, 8, 9 };
	int* array[3] = {a,b,c};		//数组名为数组首元素地址,也就是指针
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d ", array[i][j]);
		}
		printf("\n");
	}
	return 0;
}
  • 解释:结合前面所说指针与数组的关系, array[i] 获取的是指针数组中的每个元素,也就是对应的a, b, c 三个一维数组,而后面通过下标访问操作符 [j] 来访问一维数组中的每个元素 。这里的理解方式和二维数组本身的一种理解方式相似,在文章中【逐步剖C】第三章-数组也有说明,感兴趣的朋友可以看看。

示意图参考:
在这里插入图片描述

本章完。

看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或有误地方的地方还恳请过路的朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值