文章目录
- Ⅰ指针的概念
 - Ⅱ 指针和指针类型
 - ⒈指针 +- 整数
 - ⒉指针的解引用
 
- Ⅲ 野指针
 - ⒈野指针成因
 - ⒉规避野指针
 
- Ⅳ 指针运算
 - ⒈指针 +- 整数
 - ⒉指针 - 指针
 - ⒊指针的关系运算
 
- Ⅴ 指针和数组
 - Ⅵ 二级指针
 - Ⅶ 指针数组
 
Ⅰ指针的概念
指针的两个要点
- 指针是内存中一个最小单元的编号,也就是地址
 - 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
 
- 总结:指针就是地址,口语中说的指针通常指的是指针变量。
 
内存是如何存放变量的?
- 要彻底搞懂指针的概念,首先要知道内存是如何存放变量的。由于内存的最小索引单元式 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;
}
 
⒉规避野指针
- 指针初始化。
 - 小心指针越界。
 - 指针指向空间释放及时置NULL。
 - 避免返回局部变量的地址。
 - 指针使用之前检查有效性。
 
指针初始化
- 在使用指针的时候,如果知道要给指针变量什么值,就一定要把这个值赋给它。
 
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 实际上是数组名,不是一个左值。
 
左值的定义
- 如果是左值的话,有两点要求:
 
- 首先要是一个用于是不你诶和定位一个存储位置的标识符。
 - 其次这个值必须是可修改的。
 
- 第一点数组名是满足的,因为数组名就是定位一个数组的位置。第二点就无法满足了,因为数组名不是变量,它只是一个地址常量,没办法修改。
 - 如果按照这个思路来写代码,应该这样修改:
 
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)
 









































