【C语言初阶(17)】初阶指针

Ⅰ指针的概念

指针的两个要点

  1. 指针是内存中一个最小单元的编号,也就是地址
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
  • 总结:指针就是地址,口语中说的指针通常指的是指针变量。

内存是如何存放变量的?

  • 要彻底搞懂指针的概念,首先要知道内存是如何存放变量的。由于内存的最小索引单元式 1 字节,所以可以把整个内存想想为一个超级大的字符数组。
  • 数组有索引下标,内存也是,只是我们把它称为地址。每个地址可以存放 1 字节的数据,所以对于占 4 字节的整型变量来说,就需要使用 4 个连续的存储单元来存放。
  • 因为编译器知道具体每一个变量名对应的存放地址,所以当读取某个变量的时候,编译器就会找到变量名所在的地址,并根据变量的类型读取相应范围的数据。
    • 如下图所示:找到变量名 f 所在的地址,并根据变量 f 的类型连续读取四个字节的地址。

在这里插入图片描述

指针和指针变量

  • 通常我们所说的指针,就是地址的意思。C 语言中有专门的指针变量用于存放指针,跟普通变量不同,指针变量存储的是一个地址
  • 指针变量,里边存放的是地址,而通过这个地址,就可以找到一个内存单元。

地址的产生

  • 访问内存首先得有地址,有了地址才能找到内存单元,那地址是哪里来的? 地址是从地址线上传过来的
  • 32 位系统中,有 32 根地址线,每根地址线上过来的信号有 0/1 两种情况;

在这里插入图片描述

  • 32 根地址线所产生的所有二进制序列的组合就有 232 ,也就是会产生 232 个地址;

在这里插入图片描述

  • 232 个地址,1个地址管理1个字节,总共能管理 232 个字节(4GB)的空间。

  • 同样的,在 64 位系统中,就有 264 个地址。

总结

  • 指针变量是用来存放地址(指针)的,地址是唯一标示一块地址空间的。
  • 指针得我大小在 32 位机器上是 4 个字节,在 64 位上是 8 个字节。

Ⅱ 指针和指针类型

  • 指针变量也有类型,它的类型就是存放的地址指向的数据类型。
  • 如下图所示,变量 a ~ g 都是普通变量,其中变量 a ~ e 和 变量 g 都是字符变量,它们所在的地址存放的都是字符类型的数据,只占 1 个字节;
  • 变量 f 是整型变量,存放的数据是一个整型,占 4 字节的的空间;
  • 还有两个指针变量—— pa 和 pf,这两个变量存放的数据是地址,在这里它们分别存放了 变量 a 和变量 f 的地址。

在这里插入图片描述

指针的具体类型

  • 当有这样的代码:
int num = 10;
p = #
  • 要将 &num(num的地址)保存到 p 中,我们知道 p 就是一个指针变量,我们在定义指针变量 p 的时候就需要给它相应的类型了。
  • 各类型指针变量的定义:
char*	pc = NULL;//char* 就是 pc 的类型
int*  	pi = NULL;
short* 	ps = NULL;
long*	pl = NULL;
float*	pf = NULL;
double*	pd = NULL;
  • 可以看出,指针的类型就是指针所指向的变量类型+ *
    • short* 说明了 ps 是一个指向 short 类型的变量的指针,*表示 ps是一个指针。
    • int* 说明了 pi 是一个指向 int 类型的变量的指针。

指针类型的意义

  • 不管是什么类型的指针,在 32 位机器上都是 4 个字节,那么定义不同的指针类型好像就显得很憨了。
  • 然鹅,C 语言并没有把所有的指针类型都整合成一个同一的类型,那么指针类型自然是有其存在的意义的:指针 ± 整数、指针的解引用

⒈指针 ± 整数

先说结论

  • 指针的类型决定了指针 ± 1 操作的时候跳过几个字节
    • int* 类型的指针 + 1 会让地址向后走 4 个字节;
    • char* 的 + 1 会向后走 1 个字节;其余同理。

举个栗子

#include <stdio.h>

int main()
{
	int a = 0;
	char b = 'c';
	int* pa = &a;
	char* pb = &b;

	printf("%p\n", &a);
	printf("%p\n", pa);
	printf("%p\n", pa + 1);//int* 类型的指针一步跨 4 字节
	printf("----------------\n");
	printf("%p\n", &b);
	printf("%p\n", pb);
	printf("%p\n", pb + 1);//char* 类型的指针一步跨 1 字节

	return 0;
}

在这里插入图片描述

⒉指针的解引用

  • 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。
    • 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
  • 对 char* 类型的指针解引用只能修改一个字节的内存(只将一个字节的空间置为 0)。

在这里插入图片描述

  • 对 int* 类型的指针 pi 解引用就能将 4 个字节的空间全部改为 0。

在这里插入图片描述

Ⅲ 野指针

野指针的概念

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

举个栗子

#include <stdio.h>

int main()
{
	int* a;
	*a = 123;

	return 0;
}
  • 类似于上面这样的代码是很危险的,因为指针变量 a 到底指向哪里,我们没办法知道。就和访问未初始化的变量一样,它的值是随机的。这在指针变量里是很危险的,因为后边代码对一个未知的地址进行赋值,那么就可能会覆盖到系统的一些关键代码。
  • 偶尔这个指针变量里随机存放的是一个合法的地址,那么接下来的赋值会导致那个位置的值莫名其妙的被修改。
  • 这种类型的 BUG 是非常难以排查的,所以在对指针进行解引用操作时,必须确保它们已经被正确的初始化了。

⒈野指针成因

1. 指针未初始化

  • 指针没有初始化,就意味着指针没有明确的指向。
  • 一个局部变量不初始化的话,放的是随机值。指针也一样,只不过放的时随机的地址。
#include <stdio.h>

int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;//非法访问内存,将随机一个地址中存的值改成 20

	return 0;
}

2. 指针越界访问

  • 一般访问数组元素都是通过下标来访问的;
  • 但有时候也会使用指针来访问,这种情况就有可能发生指针的越界访问了。
int main()
{
	int i = 0;
	int arr[10] = { 0 };	//数组名就是首元素地址 arr = &arr[0]
	int* p = arr;			//p 指向了数组的第一个元素,p + 1 指向第二个元素

	for (i = 0; i <= 10; i++)
	{
		*p = i;
		p++;				//弄到最后会让 p + 10 相当于 arr[10],直接就越界了
	}

	return 0;
}

3. 指针指向的空间释放

int* test()
{
	int a = 10;
	return &a;

	//因为 a 是局部变量,当把 a 的地址返回之后,变量 a 自动销毁
	//原来的地址存放的就不再是变量 a 了,
}

int main()
{
	int* p = test();

	//用 p 来接收返回的 a 的地址
	//当接收了 p 的地址后,再往下使用 p 就成了野指针
	//当变量 a 销毁之后,p 还是记得传过来的地址,但此时这个地址已经不再指向 a 了
	//当 p 用这个地址往回找的时候,就不晓得找的到底是谁了

	return 0;
}

⒉规避野指针

  1. 指针初始化。
  2. 小心指针越界。
  3. 指针指向空间释放及时置NULL。
  4. 避免返回局部变量的地址。
  5. 指针使用之前检查有效性。

指针初始化

  • 在使用指针的时候,如果知道要给指针变量什么值,就一定要把这个值赋给它。
int a = 110;
int* p1 = &a;//明确把 a 的地址赋给 p1
  • 有时候确实不知道该让某个指针指向哪里的时候,一定要将其置为空指针。
int* p2 = NULL;//p2 哪都没指向,它是个空指针

指针指向空间释放及时置NULL

free(p);	//释放 p 所指向的空间
p = NULL;	//释放完之后要及时将该指针置空

指针使用之前检查有效性

if(p != NULL)
{
	*p = 100;//不是空指针就可以对 p 进行解引用
}

Ⅳ 指针运算

⒈指针 ± 整数

  • 当指针指向数组元素的时候,允许对指针变量进行加减运算,这样做的意义相当于指向举例指针所在位置向前或向后第 n 个元素。
    • 例如:p + 1 表示指向 p 指针所指向的元素的下一个元素,p - 1 则表示指向上一个元素。

在这里插入图片描述

⒉指针 - 指针

  • 指针之间也可以进行减法运算。
  • 指针 - 指针得到的绝对值是指针和指针之间元素的个数
  • 注意:指向同一块空间(同个数组)的两个指针才能相减。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

指针 - 指针的用途

  • 求字符串长度
  • 只要拿到 \0 的地址,以及首字符的地址,两个指针相减,就是字符的长度。

在这里插入图片描述

⒊指针的关系运算

  • 指针之间也可以比较大小。
#define N_VALUES 5
float values[N_VALUES];
float *vp;

for (vp = &values[0]; vp < &values[N_VALUES];)
{
     *vp++ = 0;
}
  • 当 vp 指向的地址小于 values[N_VALUES] 的地址时,就会让 vp 一直+1,指向数组的下一个元素。

在这里插入图片描述

Ⅴ 指针和数组

  • 指针和数组在很多方面都可以相互替换,给人的感觉它们似乎是一样的东西。
  • 然而,指针终归是指针,数组终归还是数组。

指针和数组的区别

  • 数组:一组相同类型元素的集合。
  • 指针变量:是一个变量,存放的是地址。

举个栗子

  • 下面代码试图计算一个字符串的字符个数:
int main()
{
	int count = 0;
	char str[] = "hello word!";

	while (*str != '\0')
	{
		str++;//数组名是个常量,不可以自增
		count++;
	}
	printf("总共有 %d 个字符\n", count);

	return 0;
}
  • 当试图运行的时候你可以看到,编译器毫不留情的报错了。

在这里插入图片描述

  • 编译器提示了自增运算符的操作对象需要一个左值,这个表达式的自增运算符的操作对象是 str ,str 实际上是数组名,不是一个左值。

左值的定义

  • 如果是左值的话,有两点要求:
  1. 是一个用于识别和定位一个存储位置的标识符。
  2. 这个值必须是可修改的。
  • 第一点数组名是满足的,因为数组名就是定位一个数组的位置。第二点就无法满足了,因为数组名不是变量,它只是一个地址常量,没办法修改
  • 如果按照这个思路来写代码,应该这样修改:
int main()
{
	int count = 0;
	char str[] = "hello word!";
	char* target = str;

	while (*target != '\0')
	{
		target++;//指针是个左值(变量),可以修改
		count++ ;
	}
	printf("总共有 %d 个字符\n",count);

	return 0;
}

在这里插入图片描述

结论

  • 数组名只是一个地址,而指针是一个左值(变量),可以存放地址

Ⅵ 二级指针

  • 指针变量也是变量,是变量就有地址,那么自然也有用来存放指针变量的地址的变量。
  • 我们管这种指针叫做:指向指针的指针(二级指针)

1. 二级指针的定义

int a = 10;
int* pa = &a;	//pa  是一级指针变量,存放整型变量 a 的地址
int** ppa = &pa;//ppa 是二级指针变量,存放指针变量 pa 的地址

在这里插入图片描述

2. 二级指针的类型

  • 二级指针的类型应该是指向的指针变量类型+*
  • 如:int** 就是二级指针 ppa 的类型。
char**	ppa = NULL;
int**	ppb = NULL;
short**	ppc = NULL;
long**	ppd = NULL;
float**	ppe = NULL;
double**ppf = NULL;

3. 二级指针解引用

  • 已经知道了对一级指针解引用一次可以找到原来变量里存的值,那么同样的,对二级指针进行两次解引用也可有找到原来变量里边存的值。

在这里插入图片描述

4. 二级指针的用途

  • 二级指针变量是用来存放一级指针变量的地址的。

Ⅶ 指针数组

  • 指针数组本质上是个数组,是用来存放指针的数组

1. 指针数组的定义

int* p[5];
//p 先和 [5] 结合,表明 p 是一个指针数组
//数组的每个元素都是一个 int* 类型的指针
  • 数组下标的优先级要比取值运算符的优先级高,所以先入为主,p 被定义为具有 5 个元素的数组。
  • 数组元素的类型是指向整型变量的指针。

在这里插入图片描述

  • p 是一个数组,有五个元素,每个元素是一个整形指针。

2. 指针数组的用途

  • 就像如果有很多 int 类型的值,可以放在一个整型数组里;
  • 同样的,如果定义的同类型的指针太多了,也可以放在指针数组里。
int a = 10;
int b = 20;
int c = 30;
......
int arr[5] = {10,20,30,......};
/可以用整型数组将多个同类型的值存储起来

int* pa = &a;
int* pb = &b;
int* pc = &c;
......
int* parr[5] = {&a,&b,&c,......};
//也可以用数组将多个同类型的指针存储起来

3. 指针数组的访问

  • 知道了怎么往指针数组里存东西之后,也要知道怎么把里面的东西拿出来。

在这里插入图片描述

  • 只要能找到数组下标为 0 的位置,就能拿到 a 的地址,再对这个地址进行解引用就可以找到 10 这个值了。其余同理
  • 先取出对应下标内存放的地址,然后再解引用
  • 现在我只想拿到前三个地址所指向的元素,请看代码:
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int* parr[5] = { &a,&b,&c };

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", *(parr[i]));
		//找到数组对应下标内存放的地址,然后解引用
	}
	putchar('\n');

	return 0;
}

在这里插入图片描述

4. 使用指针数组模拟二维数组

  • 一般的一维数组的数组名就是首元素的地址,那么如果把数组名放到指针数组里自然就能形成二维数组的效果。
int arr1[4] = { 1,2,3,4 };
int arr2[4] = { 2,2,3,4 };
int arr3[4] = { 3,2,3,4 };

int* parr[3] = { arr1,arr2,arr3 };
  • 将三个一维数组关联起来,造成一种二维数组的感觉。

在这里插入图片描述

  • 想把这些元素打印出来,依然可以使用二维数组的方式。
int main()
{
	int arr1[4] = { 1,2,3,4 };//第一行
	int arr2[4] = { 2,2,3,4 };//第二行
	int arr3[4] = { 3,2,3,4 };//第三行

	int* parr[3] = { arr1,arr2,arr3 };
	//parr[i],访问指针数组的每个元素的时候,
	//就相当于拿到了上面三行每一行的第一个元素

	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", parr[i][j]);
		}
		putchar('\n');
	}

	return 0;
}

在这里插入图片描述

  • 此处可能有人会好奇了,为什么不解引用呢?
  • 因为 [ ] 就是解引用:arr[i] <==> *(arr + i)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值